Files
ctrld/cmd/cli/upstream_probe_journal_test.go
T
Cuong Manh Le 48fa94eb7f test(cli): scope probe journal assertions to their own log lines
captureDebugMainLog swaps the process-wide mainLog, so every
mainLog.Load().Warn()/.Error() site in the package writes into the
buffer while it is installed, including goroutines left running by
other tests. retainedProbeLines returned every retained line and the
tests asserted field values over all of them, so one unrelated warn
failed the run.

Test_checkDnsLoopKeepsACustomKeyOutOfTheJournal hit this on Windows,
where the loop probe held the buffer open for two seconds:

    upstream_probe_journal_test.go: field "upstream": got , want upstream.custom

The sibling tests were exposed the same way; they index retained[0]
after a strict length check, so a stray line fails them as got 2, want 1.

Let retainedProbeLines take the messages a caller owns and return only
those lines. wantNoProbeSecret keeps scanning every retained line,
which is what that leak check is for.
2026-09-28 16:53:02 +07:00

158 lines
5.4 KiB
Go

package cli
import (
"errors"
"strings"
"testing"
"time"
"github.com/Control-D-Inc/ctrld"
)
// probeSecret is the operator text that a custom upstream key, name, and
// endpoint hold in these tests.
const probeSecret = "org-v1-SECRET0123"
// customProbeUpstream builds an upstream whose key, name, and endpoint all
// hold operator text.
func customProbeUpstream() (string, *ctrld.UpstreamConfig) {
return upstreamPrefix + probeSecret, &ctrld.UpstreamConfig{
Name: probeSecret,
Endpoint: "https://dns.example.com/" + probeSecret,
}
}
// retainedProbeLines returns the captured lines that the journal keeps. When
// messages are given, only the lines that start with one of them come back.
// captureDebugMainLog swaps the process-wide mainLog, so any goroutine of
// another test can add a warn or error line to the same buffer; a caller that
// asserts on field values names the lines its own probe wrote.
func retainedProbeLines(t *testing.T, logs *syncBuffer, messages ...string) []map[string]any {
t.Helper()
var retained []map[string]any
for _, event := range jsonLogEvents(t, logs, "") {
switch event["level"] {
case "error", "warn":
default:
continue
}
if len(messages) == 0 {
retained = append(retained, event)
continue
}
message, _ := event["message"].(string)
for _, want := range messages {
if strings.HasPrefix(message, want) {
retained = append(retained, event)
break
}
}
}
return retained
}
// wantNoProbeSecret fails when a retained line carries the operator text.
func wantNoProbeSecret(t *testing.T, logs *syncBuffer) {
t.Helper()
for _, event := range retainedProbeLines(t, logs) {
for field, value := range event {
text, ok := value.(string)
if ok && strings.Contains(text, probeSecret) {
t.Fatalf("retained field %q holds the operator text: %s", field, text)
}
}
}
}
// Test_upstreamFailureLogKeepsACustomKeyOutOfTheJournal drives one recovery
// check failure against an operator-chosen upstream key. The journal keeps
// error lines, so the key, the name, and the endpoint must stay off the line.
func Test_upstreamFailureLogKeepsACustomKeyOutOfTheJournal(t *testing.T) {
logs := captureDebugMainLog(t)
upstream, uc := customProbeUpstream()
(&upstreamFailureLog{}).report(upstream, uc, errors.New("probe failed"), time.Second)
retained := retainedProbeLines(t, logs, "Upstream check failed after ")
if len(retained) != 1 {
t.Fatalf("retained lines: got %d, want 1", len(retained))
}
wantField(t, retained[0], "upstream", upstreamPrefix+"custom")
wantNoProbeSecret(t, logs)
if !strings.Contains(logs.String(), probeSecret) {
t.Fatal("the operator text must stay at debug level for diagnosis")
}
}
// Test_checkUpstreamOnceKeepsACustomKeyOutOfTheJournal drives the resolver
// creation failure of a recovery check. It logs at error level, so the journal
// keeps it.
func Test_checkUpstreamOnceKeepsACustomKeyOutOfTheJournal(t *testing.T) {
logs := captureDebugMainLog(t)
upstream, uc := customProbeUpstream()
// An empty type has no resolver, so the check fails before it sends.
uc.Type = ""
p := &prog{}
if err := p.checkUpstreamOnce(upstream, uc, &upstreamFailureLog{}); err == nil {
t.Fatal("expected the resolver creation to fail")
}
retained := retainedProbeLines(t, logs, "Failed to create resolver")
if len(retained) != 1 {
t.Fatalf("retained lines: got %d, want 1", len(retained))
}
wantField(t, retained[0], "upstream", upstreamPrefix+"custom")
wantNoProbeSecret(t, logs)
}
// Test_checkDnsLoopKeepsACustomKeyOutOfTheJournal drives the loop checker
// against an operator-chosen upstream key. The loop checker writes at warn
// level, so the journal keeps its line too.
func Test_checkDnsLoopKeepsACustomKeyOutOfTheJournal(t *testing.T) {
logs := captureDebugMainLog(t)
uc := &ctrld.UpstreamConfig{
Name: probeSecret,
Type: ctrld.ResolverTypeLegacy,
Endpoint: deadUpstreamEndpoint,
Timeout: 200,
}
uc.Init()
cfg := &ctrld.Config{Upstream: map[string]*ctrld.UpstreamConfig{probeSecret: uc}}
p := &prog{cfg: cfg, um: newUpstreamMonitor(cfg), loop: make(map[string]bool)}
p.checkDnsLoop()
retained := retainedProbeLines(t, logs, "could not perform loop check", "could not send DNS loop check query")
if len(retained) == 0 {
t.Fatal("the loop check failure must stay visible above debug")
}
for _, event := range retained {
wantField(t, event, "upstream", upstreamPrefix+"custom")
}
wantNoProbeSecret(t, logs)
}
// Test_logUpstreamPingFailuresKeepsACustomKeyOutOfTheJournal drives the report
// that follows a failed self-check. It writes at error level, so the journal
// keeps it and the operator text must stay off the line.
func Test_logUpstreamPingFailuresKeepsACustomKeyOutOfTheJournal(t *testing.T) {
logs := captureDebugMainLog(t)
_, uc := customProbeUpstream()
origPing := upstreamPingFn
t.Cleanup(func() { upstreamPingFn = origPing })
upstreamPingFn = func(*ctrld.UpstreamConfig) error { return errors.New("ping failed") }
logUpstreamPingFailures(map[string]*ctrld.UpstreamConfig{probeSecret: uc})
retained := retainedProbeLines(t, logs, "failed to connect to the upstream")
if len(retained) != 1 {
t.Fatalf("retained lines: got %d, want 1", len(retained))
}
wantField(t, retained[0], "upstream", upstreamPrefix+"custom")
wantNoProbeSecret(t, logs)
if !strings.Contains(logs.String(), probeSecret) {
t.Fatal("the operator text must stay at debug level for diagnosis")
}
}