186 lines
4.5 KiB
Go
186 lines
4.5 KiB
Go
package core
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// These tests exist to give `go test -race` something to chew on. The GUI
|
|
// reads every one of these structures from its frame loop while background
|
|
// goroutines write to them, which is exactly the shape of bug that never
|
|
// shows up in a single-threaded test.
|
|
|
|
func TestLogBufferConcurrentAccess(t *testing.T) {
|
|
buf := NewLogBuffer(128) // small, so eviction runs constantly
|
|
logger := slog.New(buf.Handler(nil))
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 400*time.Millisecond)
|
|
defer cancel()
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
// Writers.
|
|
for i := 0; i < 4; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
l := logger.With("from", "writer", "id", id)
|
|
for ctx.Err() == nil {
|
|
l.Info("message", "n", id, "auth_key", "tskey-auth-SECRETVALUE123")
|
|
l.Debug("detail", slog.Group("g", slog.String("k", "v")))
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
// Readers, mimicking the GUI's frame loop.
|
|
for i := 0; i < 3; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for ctx.Err() == nil {
|
|
_ = buf.Tail(50)
|
|
_ = buf.Filter(LogQuery{MinLevel: slog.LevelInfo, Text: "message", Limit: 20})
|
|
_ = buf.Sources()
|
|
_ = buf.Counts()
|
|
_ = buf.Len()
|
|
_ = buf.Dropped()
|
|
_ = buf.LastSeq()
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Subscribers churning in and out.
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for ctx.Err() == nil {
|
|
ch, cancelSub := buf.Subscribe()
|
|
select {
|
|
case <-ch:
|
|
case <-time.After(5 * time.Millisecond):
|
|
}
|
|
cancelSub()
|
|
}
|
|
}()
|
|
|
|
// Exporter, which walks the whole ring and redacts.
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for ctx.Err() == nil {
|
|
out := buf.ExportText(ExportOptions{Query: LogQuery{MinLevel: slog.LevelDebug, Limit: 100}})
|
|
if len(out) > 0 && containsSecret(out) {
|
|
t.Error("export leaked an auth key")
|
|
return
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
}()
|
|
|
|
wg.Wait()
|
|
|
|
if buf.Len() > 128 {
|
|
t.Fatalf("ring exceeded its capacity: %d", buf.Len())
|
|
}
|
|
if buf.Dropped() == 0 {
|
|
t.Fatal("expected eviction to have occurred")
|
|
}
|
|
}
|
|
|
|
func containsSecret(s string) bool {
|
|
return len(s) > 0 && (indexOf(s, "SECRETVALUE123") >= 0)
|
|
}
|
|
|
|
func indexOf(hay, needle string) int {
|
|
for i := 0; i+len(needle) <= len(hay); i++ {
|
|
if hay[i:i+len(needle)] == needle {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func TestLogBufferTailOrderAndBounds(t *testing.T) {
|
|
buf := NewLogBuffer(4)
|
|
logger := slog.New(buf.Handler(nil))
|
|
for i := 0; i < 10; i++ {
|
|
logger.Info("m", "i", i)
|
|
}
|
|
|
|
got := buf.Tail(3)
|
|
if len(got) != 3 {
|
|
t.Fatalf("Tail(3) returned %d entries", len(got))
|
|
}
|
|
// Oldest first, and the newest must be last.
|
|
for i := 1; i < len(got); i++ {
|
|
if got[i].Seq <= got[i-1].Seq {
|
|
t.Fatalf("Tail is not in chronological order: %v", got)
|
|
}
|
|
}
|
|
if got[len(got)-1].Seq != buf.LastSeq() {
|
|
t.Fatalf("Tail did not end at the newest record")
|
|
}
|
|
if n := len(buf.Tail(100)); n != 4 {
|
|
t.Fatalf("Tail beyond capacity returned %d, want 4", n)
|
|
}
|
|
if n := len(buf.Tail(0)); n != 0 {
|
|
t.Fatalf("Tail(0) returned %d entries", n)
|
|
}
|
|
}
|
|
|
|
func TestLogBufferRedactsOnExport(t *testing.T) {
|
|
buf := NewLogBuffer(16)
|
|
logger := slog.New(buf.Handler(nil))
|
|
logger.Info("joining", "auth_key", "tskey-auth-kSomeRealLookingKey123")
|
|
logger.Info("inline", "url", "https://x/?k=tskey-client-abcdefghijkl")
|
|
|
|
out := buf.ExportText(ExportOptions{Query: LogQuery{MinLevel: slog.LevelDebug}})
|
|
if indexOf(out, "kSomeRealLookingKey123") >= 0 {
|
|
t.Error("attribute-named secret survived redaction")
|
|
}
|
|
if indexOf(out, "abcdefghijkl") >= 0 {
|
|
t.Error("inline tskey survived redaction")
|
|
}
|
|
|
|
raw := buf.ExportText(ExportOptions{Query: LogQuery{MinLevel: slog.LevelDebug}, NoRedact: true})
|
|
if indexOf(raw, "kSomeRealLookingKey123") < 0 {
|
|
t.Error("NoRedact should preserve the original text")
|
|
}
|
|
}
|
|
|
|
func TestPeerMonitorSnapshotIsIsolated(t *testing.T) {
|
|
m := NewPeerMonitor(nil, nil, slog.New(slog.DiscardHandler), PeerMonitorOptions{})
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond)
|
|
defer cancel()
|
|
// A nil server must not panic; the monitor should degrade to an invalid
|
|
// snapshot with an error rather than taking the GUI down.
|
|
m.Start(ctx)
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 4; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for ctx.Err() == nil {
|
|
snap := m.Snapshot()
|
|
for _, p := range snap.Peers {
|
|
_ = p.DisplayName
|
|
_ = len(p.Samples)
|
|
}
|
|
_ = m.History("nonexistent")
|
|
m.RefreshNow()
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
snap := m.Snapshot()
|
|
if snap.Valid {
|
|
t.Error("snapshot from a nil tsnet server should not be valid")
|
|
}
|
|
}
|