1 Commits

Author SHA1 Message Date
3959aedb6a Remove DNS mocking from tests; use live DNS everywhere
All checks were successful
check / check (push) Successful in 50s
DNS is never mocked in this repository: tests exercise live DNS,
and robustness comes from handling real-world DNS behavior with
tolerant assertions and sensible timeouts, not from mocks.

watcher: drop mockResolver and wire the real iterative resolver
into the tests, querying stable public names (example.com,
www.example.com). Change detection is exercised by seeding the
state store with a synthetic previous observation that live DNS
cannot match (reserved .invalid nameserver names and RFC 5737
documentation addresses); DNS stays live in every run. The port
checker, TLS checker, and notifier remain test doubles since they
are not DNS, keeping notification and state assertions
deterministic against whatever addresses live DNS returns.

resolver: drop the timeoutClient fake DNSClient and the
NewFromLoggerWithClient mock constructor. The timeout test is
replaced by a live query against an RFC 5737 documentation
address where no nameserver can exist, asserting a classified
non-OK response with no records.

TESTING.md: extend the live-DNS policy to every package and
remove the carve-out that permitted DNS mocks in packages that
consume the resolver.

TODO.md: update stale references to hermetic mocked-DNS work to
reflect the no-mocking policy and the current state of
feature/resolver.

Intentionally dropped coverage: the exact StatusTimeout
classification (previously forced by the fake client) is no
longer asserted, because a genuinely unreachable server may fail
fast instead of timing out depending on the network path; the
live test tolerantly accepts any failure classification.
2026-08-07 20:43:09 +00:00
11 changed files with 195 additions and 244 deletions

View File

@@ -1,9 +1,5 @@
version: "2" version: "2"
# Config schema uses the golangci-lint v2 layout (settings live under
# linters.settings, not top-level linters-settings) so that the
# thresholds below are actually applied by golangci-lint >= v2.
run: run:
timeout: 5m timeout: 5m
modules-download-mode: readonly modules-download-mode: readonly
@@ -18,7 +14,8 @@ linters:
- wsl # Deprecated, replaced by wsl_v5 - wsl # Deprecated, replaced by wsl_v5
- wrapcheck # Too verbose for internal packages - wrapcheck # Too verbose for internal packages
- varnamelen # Short names like db, id are idiomatic Go - varnamelen # Short names like db, id are idiomatic Go
settings:
linters-settings:
lll: lll:
line-length: 88 line-length: 88
funlen: funlen:
@@ -30,5 +27,6 @@ linters:
threshold: 100 threshold: 100
issues: issues:
exclude-use-default: false
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0

View File

@@ -4,8 +4,8 @@ FROM golang@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4
RUN apk add --no-cache git make gcc musl-dev binutils-gold RUN apk add --no-cache git make gcc musl-dev binutils-gold
# golangci-lint v2.12.2, 2026-08-07 # golangci-lint v2.10.1
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@c0d3ddc9cf3faa61a4e378e879ece580256d76e5 RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@5d1e709b7be35cb2025444e19de266b056b7b7ee
# goimports v0.42.0 # goimports v0.42.0
RUN go install golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0 RUN go install golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0

View File

@@ -17,26 +17,6 @@ without requiring an external database.
--- ---
## No DNS mocking. Ever.
**DNS is never mocked in this project — not in tests, not anywhere else.**
No mock resolvers, no fake DNS servers, no stubbed lookups.
dnswatcher's entire purpose is correct behavior against the real DNS.
Tests exercise real iterative resolution against live nameservers by
design; a test suite that passes against a mock proves nothing about the
one thing this program exists to do.
When live tests are flaky, that is a robustness problem, and it gets
fixed with robustness: retries with backoff, querying multiple
independent nameservers, longer timeouts — or explicit opt-in gating
decided by the project owner. Never with mocks.
Contributions that introduce mocked, faked, or stubbed DNS will be
rejected.
---
## Features ## Features
### DNS Domain Monitoring (Apex Domains) ### DNS Domain Monitoring (Apex Domains)

View File

@@ -32,14 +32,6 @@ confirm make check still passes.
tests now drive the real iterative resolver against live DNS and tests now drive the real iterative resolver against live DNS and
`TESTING.md` bans DNS mocks in every package (`remove-dns-mocking` `TESTING.md` bans DNS mocks in every package (`remove-dns-mocking`
branch) branch)
- 2026-08-07: golangci-lint bumped to v2.12.2 (commit-pinned installs
in `Dockerfile` and `script/bootstrap`); `.golangci.yml` set to the
org-standard v2-schema config used across the org's repos
(owner-authorized; same file is being landed as canonical via prompts
PR #24), with settings under `linters.settings` so the
lll/funlen/cyclop/dupl thresholds apply; fixed the resulting
`goconst`, `dupl`, and `lll` findings; the informational `gomodguard`
deprecation warning under this config is accepted
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, - 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints,
Makefile shims, README Entrypoints section Makefile shims, README Entrypoints section
- 2026-02-20: iterative DNS resolver implemented; tests made hermetic - 2026-02-20: iterative DNS resolver implemented; tests made hermetic

View File

@@ -17,33 +17,13 @@ func TestClassifyDNSName(t *testing.T) {
}{ }{
{name: "apex domain simple", input: "example.com", want: config.DNSNameTypeDomain}, {name: "apex domain simple", input: "example.com", want: config.DNSNameTypeDomain},
{name: "hostname simple", input: "www.example.com", want: config.DNSNameTypeHostname}, {name: "hostname simple", input: "www.example.com", want: config.DNSNameTypeHostname},
{ {name: "apex domain multi-part TLD", input: "example.co.uk", want: config.DNSNameTypeDomain},
name: "apex domain multi-part TLD", {name: "hostname multi-part TLD", input: "api.example.co.uk", want: config.DNSNameTypeHostname},
input: "example.co.uk",
want: config.DNSNameTypeDomain,
},
{
name: "hostname multi-part TLD",
input: "api.example.co.uk",
want: config.DNSNameTypeHostname,
},
{name: "public suffix itself", input: "co.uk", wantErr: true}, {name: "public suffix itself", input: "co.uk", wantErr: true},
{name: "empty string", input: "", wantErr: true}, {name: "empty string", input: "", wantErr: true},
{ {name: "deeply nested hostname", input: "a.b.c.example.com", want: config.DNSNameTypeHostname},
name: "deeply nested hostname", {name: "trailing dot stripped", input: "example.com.", want: config.DNSNameTypeDomain},
input: "a.b.c.example.com", {name: "uppercase normalized", input: "WWW.Example.COM", want: config.DNSNameTypeHostname},
want: config.DNSNameTypeHostname,
},
{
name: "trailing dot stripped",
input: "example.com.",
want: config.DNSNameTypeDomain,
},
{
name: "uppercase normalized",
input: "WWW.Example.COM",
want: config.DNSNameTypeHostname,
},
} }
for _, tt := range tests { for _, tt := range tests {

View File

@@ -25,20 +25,6 @@ const (
colorDefault = "#6c757d" colorDefault = "#6c757d"
) )
// Priority strings used across multiple tests.
const (
prioError = "error"
prioWarning = "warning"
prioSuccess = "success"
prioInfo = "info"
prioUnknown = "unknown"
prioDefault = "default"
prioUrgent = "urgent"
)
// testHost is the hostname used in request construction tests.
const testHost = "example.com"
// errSimulated is a static error for transport failures. // errSimulated is a static error for transport failures.
var errSimulated = errors.New("simulated transport failure") var errSimulated = errors.New("simulated transport failure")
@@ -115,13 +101,13 @@ func TestNtfyPriority(t *testing.T) {
input string input string
want string want string
}{ }{
{prioError, prioUrgent}, {"error", "urgent"},
{prioWarning, "high"}, {"warning", "high"},
{prioSuccess, prioDefault}, {"success", "default"},
{prioInfo, "low"}, {"info", "low"},
{"", prioDefault}, {"", "default"},
{prioUnknown, prioDefault}, {"unknown", "default"},
{"critical", prioDefault}, {"critical", "default"},
} }
for _, tc := range cases { for _, tc := range cases {
@@ -148,12 +134,12 @@ func TestSlackColor(t *testing.T) {
input string input string
want string want string
}{ }{
{prioError, colorError}, {"error", colorError},
{prioWarning, colorWarning}, {"warning", colorWarning},
{prioSuccess, colorSuccess}, {"success", colorSuccess},
{prioInfo, colorInfo}, {"info", colorInfo},
{"", colorDefault}, {"", colorDefault},
{prioUnknown, colorDefault}, {"unknown", colorDefault},
{"critical", colorDefault}, {"critical", colorDefault},
} }
@@ -179,7 +165,7 @@ func TestNewRequest(t *testing.T) {
target := &url.URL{ target := &url.URL{
Scheme: "https", Scheme: "https",
Host: testHost, Host: "example.com",
Path: "/webhook", Path: "/webhook",
} }
body := bytes.NewBufferString("hello") body := bytes.NewBufferString("hello")
@@ -201,9 +187,9 @@ func TestNewRequest(t *testing.T) {
) )
} }
if req.Host != testHost { if req.Host != "example.com" {
t.Errorf( t.Errorf(
"Host = %q, want %q", req.Host, testHost, "Host = %q, want %q", req.Host, "example.com",
) )
} }
@@ -231,7 +217,7 @@ func TestNewRequestPreservesContext(t *testing.T) {
ctxKey("k"), ctxKey("k"),
"v", "v",
) )
target := &url.URL{Scheme: "https", Host: testHost} target := &url.URL{Scheme: "https", Host: "example.com"}
req := notify.NewRequestForTest( req := notify.NewRequestForTest(
ctx, http.MethodGet, target, http.NoBody, ctx, http.MethodGet, target, http.NoBody,
@@ -303,10 +289,10 @@ func TestSendNtfyHeaders(t *testing.T) {
) )
} }
if captured.priority != prioUrgent { if captured.priority != "urgent" {
t.Errorf( t.Errorf(
"Priority header = %q, want %q", "Priority header = %q, want %q",
captured.priority, prioUrgent, captured.priority, "urgent",
) )
} }
@@ -325,10 +311,10 @@ func TestSendNtfyAllPriorities(t *testing.T) {
input string input string
want string want string
}{ }{
{prioError, prioUrgent}, {"error", "urgent"},
{prioWarning, "high"}, {"warning", "high"},
{prioSuccess, prioDefault}, {"success", "default"},
{prioInfo, "low"}, {"info", "low"},
} }
for _, tc := range priorities { for _, tc := range priorities {
@@ -370,69 +356,56 @@ func TestSendNtfyAllPriorities(t *testing.T) {
} }
} }
// assertSendStatusError verifies that send returns an error func TestSendNtfyClientError(t *testing.T) {
// wrapping wantErr when the server responds with status. t.Parallel()
func assertSendStatusError(
t *testing.T,
status int,
wantErr error,
send func(*notify.Service, *url.URL) error,
) {
t.Helper()
srv := httptest.NewServer( srv := httptest.NewServer(
http.HandlerFunc( http.HandlerFunc(
func(w http.ResponseWriter, _ *http.Request) { func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(status) w.WriteHeader(http.StatusForbidden)
}), }),
) )
defer srv.Close() defer srv.Close()
svc := notify.NewTestService(srv.Client().Transport) svc := notify.NewTestService(srv.Client().Transport)
target, _ := url.Parse(srv.URL) topicURL, _ := url.Parse(srv.URL)
err := send(svc, target) err := svc.SendNtfy(
context.Background(), topicURL, "t", "m", "info",
)
if err == nil { if err == nil {
t.Fatalf("expected error for %d response", status) t.Fatal("expected error for 403 response")
} }
if !errors.Is(err, wantErr) { if !errors.Is(err, notify.ErrNtfyFailed) {
t.Errorf("error = %v, want %v", err, wantErr) t.Errorf("error = %v, want ErrNtfyFailed", err)
} }
} }
func sendNtfyInfo(
svc *notify.Service, target *url.URL,
) error {
return svc.SendNtfy(
context.Background(), target, "t", "m", prioInfo,
)
}
func sendSlackInfo(
svc *notify.Service, target *url.URL,
) error {
return svc.SendSlack(
context.Background(), target, "t", "m", prioInfo,
)
}
func TestSendNtfyClientError(t *testing.T) {
t.Parallel()
assertSendStatusError(
t, http.StatusForbidden,
notify.ErrNtfyFailed, sendNtfyInfo,
)
}
func TestSendNtfyServerError(t *testing.T) { func TestSendNtfyServerError(t *testing.T) {
t.Parallel() t.Parallel()
assertSendStatusError( srv := httptest.NewServer(
t, http.StatusInternalServerError, http.HandlerFunc(
notify.ErrNtfyFailed, sendNtfyInfo, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}),
) )
defer srv.Close()
svc := notify.NewTestService(srv.Client().Transport)
topicURL, _ := url.Parse(srv.URL)
err := svc.SendNtfy(
context.Background(), topicURL, "t", "m", "info",
)
if err == nil {
t.Fatal("expected error for 500 response")
}
if !errors.Is(err, notify.ErrNtfyFailed) {
t.Errorf("error = %v, want ErrNtfyFailed", err)
}
} }
func TestSendNtfySuccess(t *testing.T) { func TestSendNtfySuccess(t *testing.T) {
@@ -577,11 +550,11 @@ func TestSendSlackAllColors(t *testing.T) {
priority string priority string
want string want string
}{ }{
{prioError, colorError}, {"error", colorError},
{prioWarning, colorWarning}, {"warning", colorWarning},
{prioSuccess, colorSuccess}, {"success", colorSuccess},
{prioInfo, colorInfo}, {"info", colorInfo},
{prioUnknown, colorDefault}, {"unknown", colorDefault},
} }
for _, tc := range colors { for _, tc := range colors {
@@ -633,19 +606,53 @@ func TestSendSlackAllColors(t *testing.T) {
func TestSendSlackClientError(t *testing.T) { func TestSendSlackClientError(t *testing.T) {
t.Parallel() t.Parallel()
assertSendStatusError( srv := httptest.NewServer(
t, http.StatusBadRequest, http.HandlerFunc(
notify.ErrSlackFailed, sendSlackInfo, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
}),
) )
defer srv.Close()
svc := notify.NewTestService(srv.Client().Transport)
webhookURL, _ := url.Parse(srv.URL)
err := svc.SendSlack(
context.Background(), webhookURL, "t", "m", "info",
)
if err == nil {
t.Fatal("expected error for 400 response")
}
if !errors.Is(err, notify.ErrSlackFailed) {
t.Errorf("error = %v, want ErrSlackFailed", err)
}
} }
func TestSendSlackServerError(t *testing.T) { func TestSendSlackServerError(t *testing.T) {
t.Parallel() t.Parallel()
assertSendStatusError( srv := httptest.NewServer(
t, http.StatusBadGateway, http.HandlerFunc(
notify.ErrSlackFailed, sendSlackInfo, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadGateway)
}),
) )
defer srv.Close()
svc := notify.NewTestService(srv.Client().Transport)
webhookURL, _ := url.Parse(srv.URL)
err := svc.SendSlack(
context.Background(), webhookURL, "t", "m", "error",
)
if err == nil {
t.Fatal("expected error for 502 response")
}
if !errors.Is(err, notify.ErrSlackFailed) {
t.Errorf("error = %v, want ErrSlackFailed", err)
}
} }
func TestSendSlackNetworkError(t *testing.T) { func TestSendSlackNetworkError(t *testing.T) {
@@ -970,62 +977,74 @@ func TestSendNotificationMattermostOnly(t *testing.T) {
} }
} }
// assertSendNotificationTolerates verifies SendNotification func TestSendNotificationNtfyError(t *testing.T) {
// neither panics nor blocks when the endpoint configured by t.Parallel()
// setURL responds with status.
func assertSendNotificationTolerates(
t *testing.T,
status int,
priority string,
setURL func(*notify.Service, *url.URL),
) {
t.Helper()
srv := httptest.NewServer( srv := httptest.NewServer(
http.HandlerFunc( http.HandlerFunc(
func(w http.ResponseWriter, _ *http.Request) { func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(status) w.WriteHeader(http.StatusInternalServerError)
}), }),
) )
defer srv.Close() defer srv.Close()
target, _ := url.Parse(srv.URL) ntfyURL, _ := url.Parse(srv.URL)
svc := notify.NewTestService(http.DefaultTransport) svc := notify.NewTestService(http.DefaultTransport)
setURL(svc, target) svc.SetNtfyURL(ntfyURL)
// Should not panic or block.
svc.SendNotification( svc.SendNotification(
context.Background(), "t", "m", priority, context.Background(), "t", "m", "error",
) )
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
} }
func TestSendNotificationNtfyError(t *testing.T) {
t.Parallel()
assertSendNotificationTolerates(
t, http.StatusInternalServerError, prioError,
(*notify.Service).SetNtfyURL,
)
}
func TestSendNotificationSlackError(t *testing.T) { func TestSendNotificationSlackError(t *testing.T) {
t.Parallel() t.Parallel()
assertSendNotificationTolerates( srv := httptest.NewServer(
t, http.StatusForbidden, prioError, http.HandlerFunc(
(*notify.Service).SetSlackWebhookURL, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusForbidden)
}),
) )
defer srv.Close()
slackURL, _ := url.Parse(srv.URL)
svc := notify.NewTestService(http.DefaultTransport)
svc.SetSlackWebhookURL(slackURL)
svc.SendNotification(
context.Background(), "t", "m", "error",
)
time.Sleep(100 * time.Millisecond)
} }
func TestSendNotificationMattermostError(t *testing.T) { func TestSendNotificationMattermostError(t *testing.T) {
t.Parallel() t.Parallel()
assertSendNotificationTolerates( srv := httptest.NewServer(
t, http.StatusBadGateway, prioWarning, http.HandlerFunc(
(*notify.Service).SetMattermostWebhookURL, func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadGateway)
}),
) )
defer srv.Close()
mmURL, _ := url.Parse(srv.URL)
svc := notify.NewTestService(http.DefaultTransport)
svc.SetMattermostWebhookURL(mmURL)
svc.SendNotification(
context.Background(), "t", "m", "warning",
)
time.Sleep(100 * time.Millisecond)
} }
// ── SlackPayload JSON marshaling ────────────────────────── // ── SlackPayload JSON marshaling ──────────────────────────

View File

@@ -29,14 +29,14 @@ func TestAlertHistoryAddAndRecent(t *testing.T) {
Timestamp: now.Add(-2 * time.Minute), Timestamp: now.Add(-2 * time.Minute),
Title: "first", Title: "first",
Message: "msg1", Message: "msg1",
Priority: prioInfo, Priority: "info",
}) })
h.Add(notify.AlertEntry{ h.Add(notify.AlertEntry{
Timestamp: now.Add(-1 * time.Minute), Timestamp: now.Add(-1 * time.Minute),
Title: "second", Title: "second",
Message: "msg2", Message: "msg2",
Priority: prioWarning, Priority: "warning",
}) })
entries := h.Recent() entries := h.Recent()

View File

@@ -69,7 +69,7 @@ func (rc RetryConfig) backoff(attempt int) time.Duration {
lo := raw * (1 - jitterFraction) lo := raw * (1 - jitterFraction)
hi := raw * (1 + jitterFraction) hi := raw * (1 + jitterFraction)
jittered := lo + rand.Float64()*(hi-lo) //nolint:gosec // jitter needs no crypto/rand jittered := lo + rand.Float64()*(hi-lo) //nolint:gosec // jitter does not need crypto/rand
return time.Duration(jittered) return time.Duration(jittered)
} }

View File

@@ -13,16 +13,6 @@ import (
const testHostname = "www.example.com" const testHostname = "www.example.com"
// Shared fixture values used across tests.
const (
testNS1 = "ns1.example.com."
testNS2 = "ns2.example.com."
testAltNS1 = "ns1.test.com."
testIPv4 = "93.184.216.34"
testIP = "1.2.3.4"
statusError = "error"
)
// populateState fills a State with representative test data across all categories. // populateState fills a State with representative test data across all categories.
func populateState(t *testing.T, s *state.State) { func populateState(t *testing.T, s *state.State) {
t.Helper() t.Helper()
@@ -30,7 +20,7 @@ func populateState(t *testing.T, s *state.State) {
now := time.Now().UTC().Truncate(time.Second) now := time.Now().UTC().Truncate(time.Second)
s.SetDomainState("example.com", &state.DomainState{ s.SetDomainState("example.com", &state.DomainState{
Nameservers: []string{testNS1, testNS2}, Nameservers: []string{"ns1.example.com.", "ns2.example.com."},
LastChecked: now, LastChecked: now,
}) })
@@ -41,17 +31,17 @@ func populateState(t *testing.T, s *state.State) {
s.SetHostnameState(testHostname, &state.HostnameState{ s.SetHostnameState(testHostname, &state.HostnameState{
RecordsByNameserver: map[string]*state.NameserverRecordState{ RecordsByNameserver: map[string]*state.NameserverRecordState{
testNS1: { "ns1.example.com.": {
Records: map[string][]string{ Records: map[string][]string{
"A": {testIPv4}, "A": {"93.184.216.34"},
"AAAA": {"2606:2800:220:1:248:1893:25c8:1946"}, "AAAA": {"2606:2800:220:1:248:1893:25c8:1946"},
}, },
Status: "ok", Status: "ok",
LastChecked: now, LastChecked: now,
}, },
testNS2: { "ns2.example.com.": {
Records: map[string][]string{ Records: map[string][]string{
"A": {testIPv4}, "A": {"93.184.216.34"},
}, },
Status: "ok", Status: "ok",
LastChecked: now, LastChecked: now,
@@ -162,13 +152,13 @@ func TestSaveLoadRoundTrip_Hostnames(t *testing.T) {
func verifyNS1Records(t *testing.T, hn *state.HostnameState) { func verifyNS1Records(t *testing.T, hn *state.HostnameState) {
t.Helper() t.Helper()
ns1, ok := hn.RecordsByNameserver[testNS1] ns1, ok := hn.RecordsByNameserver["ns1.example.com."]
if !ok { if !ok {
t.Fatal("missing nameserver ns1.example.com.") t.Fatal("missing nameserver ns1.example.com.")
} }
aRecords := ns1.Records["A"] aRecords := ns1.Records["A"]
if len(aRecords) != 1 || aRecords[0] != testIPv4 { if len(aRecords) != 1 || aRecords[0] != "93.184.216.34" {
t.Errorf("ns1 A records: got %v", aRecords) t.Errorf("ns1 A records: got %v", aRecords)
} }
@@ -223,8 +213,7 @@ func TestSaveLoadRoundTrip_Ports(t *testing.T) {
} }
} }
// TestSaveLoadRoundTrip_Certificates verifies certificate data // TestSaveLoadRoundTrip_Certificates verifies certificate data survives a save/load cycle.
// survives a save/load cycle.
func TestSaveLoadRoundTrip_Certificates(t *testing.T) { func TestSaveLoadRoundTrip_Certificates(t *testing.T) {
t.Parallel() t.Parallel()
@@ -664,7 +653,7 @@ func TestDomainState_GetSet(t *testing.T) {
now := time.Now().UTC().Truncate(time.Second) now := time.Now().UTC().Truncate(time.Second)
ds := &state.DomainState{ ds := &state.DomainState{
Nameservers: []string{testAltNS1}, Nameservers: []string{"ns1.test.com."},
LastChecked: now, LastChecked: now,
} }
@@ -675,7 +664,7 @@ func TestDomainState_GetSet(t *testing.T) {
t.Fatal("expected true for existing domain") t.Fatal("expected true for existing domain")
} }
if len(got.Nameservers) != 1 || got.Nameservers[0] != testAltNS1 { if len(got.Nameservers) != 1 || got.Nameservers[0] != "ns1.test.com." {
t.Errorf("nameservers: got %v", got.Nameservers) t.Errorf("nameservers: got %v", got.Nameservers)
} }
@@ -685,7 +674,7 @@ func TestDomainState_GetSet(t *testing.T) {
// Overwrite. // Overwrite.
ds2 := &state.DomainState{ ds2 := &state.DomainState{
Nameservers: []string{testAltNS1, "ns2.test.com."}, Nameservers: []string{"ns1.test.com.", "ns2.test.com."},
LastChecked: now.Add(time.Hour), LastChecked: now.Add(time.Hour),
} }
@@ -715,8 +704,8 @@ func TestHostnameState_GetSet(t *testing.T) {
now := time.Now().UTC().Truncate(time.Second) now := time.Now().UTC().Truncate(time.Second)
hs := &state.HostnameState{ hs := &state.HostnameState{
RecordsByNameserver: map[string]*state.NameserverRecordState{ RecordsByNameserver: map[string]*state.NameserverRecordState{
testNS1: { "ns1.example.com.": {
Records: map[string][]string{"A": {testIP}}, Records: map[string][]string{"A": {"1.2.3.4"}},
Status: "ok", Status: "ok",
LastChecked: now, LastChecked: now,
}, },
@@ -731,7 +720,7 @@ func TestHostnameState_GetSet(t *testing.T) {
t.Fatal("expected true for existing hostname") t.Fatal("expected true for existing hostname")
} }
nsState, ok := got.RecordsByNameserver[testNS1] nsState, ok := got.RecordsByNameserver["ns1.example.com."]
if !ok { if !ok {
t.Fatal("missing nameserver entry") t.Fatal("missing nameserver entry")
} }
@@ -741,7 +730,7 @@ func TestHostnameState_GetSet(t *testing.T) {
} }
aRecords := nsState.Records["A"] aRecords := nsState.Records["A"]
if len(aRecords) != 1 || aRecords[0] != testIP { if len(aRecords) != 1 || aRecords[0] != "1.2.3.4" {
t.Errorf("A records: got %v", aRecords) t.Errorf("A records: got %v", aRecords)
} }
} }
@@ -880,7 +869,7 @@ func TestCertificateState_ErrorField(t *testing.T) {
now := time.Now().UTC().Truncate(time.Second) now := time.Now().UTC().Truncate(time.Second)
cs := &state.CertificateState{ cs := &state.CertificateState{
Status: statusError, Status: "error",
Error: "connection refused", Error: "connection refused",
LastChecked: now, LastChecked: now,
} }
@@ -904,8 +893,8 @@ func TestCertificateState_ErrorField(t *testing.T) {
t.Fatal("missing certificate after load") t.Fatal("missing certificate after load")
} }
if got.Status != statusError { if got.Status != "error" {
t.Errorf("status: got %q, want %q", got.Status, statusError) t.Errorf("status: got %q, want %q", got.Status, "error")
} }
if got.Error != "connection refused" { if got.Error != "connection refused" {
@@ -923,9 +912,9 @@ func TestHostnameState_ErrorField(t *testing.T) {
now := time.Now().UTC().Truncate(time.Second) now := time.Now().UTC().Truncate(time.Second)
hs := &state.HostnameState{ hs := &state.HostnameState{
RecordsByNameserver: map[string]*state.NameserverRecordState{ RecordsByNameserver: map[string]*state.NameserverRecordState{
testNS1: { "ns1.example.com.": {
Records: nil, Records: nil,
Status: statusError, Status: "error",
Error: "SERVFAIL", Error: "SERVFAIL",
LastChecked: now, LastChecked: now,
}, },
@@ -952,9 +941,9 @@ func TestHostnameState_ErrorField(t *testing.T) {
t.Fatal("missing hostname after load") t.Fatal("missing hostname after load")
} }
nsState := got.RecordsByNameserver[testNS1] nsState := got.RecordsByNameserver["ns1.example.com."]
if nsState.Status != statusError { if nsState.Status != "error" {
t.Errorf("status: got %q, want %q", nsState.Status, statusError) t.Errorf("status: got %q, want %q", nsState.Status, "error")
} }
if nsState.Error != "SERVFAIL" { if nsState.Error != "SERVFAIL" {
@@ -1073,8 +1062,7 @@ func TestConcurrentGetSet(t *testing.T) {
wg.Wait() wg.Wait()
} }
// runConcurrentOps performs a series of get/set/delete // runConcurrentOps performs a series of get/set/delete operations for concurrency testing.
// operations for concurrency testing.
func runConcurrentOps(s *state.State, key string, now time.Time) { func runConcurrentOps(s *state.State, key string, now time.Time) {
const iterations = 50 const iterations = 50
@@ -1097,7 +1085,7 @@ func runConcurrentOps(s *state.State, key string, now time.Time) {
s.SetHostnameState(key+".example.com", &state.HostnameState{ s.SetHostnameState(key+".example.com", &state.HostnameState{
RecordsByNameserver: map[string]*state.NameserverRecordState{ RecordsByNameserver: map[string]*state.NameserverRecordState{
"ns1.test.": { "ns1.test.": {
Records: map[string][]string{"A": {testIP}}, Records: map[string][]string{"A": {"1.2.3.4"}},
Status: "ok", Status: "ok",
LastChecked: now, LastChecked: now,
}, },

View File

@@ -26,12 +26,6 @@ const tlsPort = 443
// hoursPerDay converts days to hours for duration calculations. // hoursPerDay converts days to hours for duration calculations.
const hoursPerDay = 24 const hoursPerDay = 24
// Status values recorded for nameserver and certificate checks.
const (
statusOK = "ok"
statusError = "error"
)
// Params contains dependencies for Watcher. // Params contains dependencies for Watcher.
type Params struct { type Params struct {
fx.In fx.In
@@ -350,7 +344,7 @@ func buildHostnameState(
for ns, recs := range records { for ns, recs := range records {
hs.RecordsByNameserver[ns] = &state.NameserverRecordState{ hs.RecordsByNameserver[ns] = &state.NameserverRecordState{
Records: recs, Records: recs,
Status: statusOK, Status: "ok",
LastChecked: now, LastChecked: now,
} }
} }
@@ -408,7 +402,7 @@ func (w *Watcher) detectNSDisappearances(
current map[string]map[string][]string, current map[string]map[string][]string,
) { ) {
for ns, prevNS := range prev.RecordsByNameserver { for ns, prevNS := range prev.RecordsByNameserver {
if _, ok := current[ns]; ok || prevNS.Status != statusOK { if _, ok := current[ns]; ok || prevNS.Status != "ok" {
continue continue
} }
@@ -427,7 +421,7 @@ func (w *Watcher) detectNSDisappearances(
for ns := range current { for ns := range current {
prevNS, ok := prev.RecordsByNameserver[ns] prevNS, ok := prev.RecordsByNameserver[ns]
if !ok || prevNS.Status != statusError { if !ok || prevNS.Status != "error" {
continue continue
} }
@@ -711,7 +705,7 @@ func (w *Watcher) handleTLSError(
now time.Time, now time.Time,
err error, err error,
) { ) {
if hasPrev && !w.firstRun && prev.Status == statusOK { if hasPrev && !w.firstRun && prev.Status == "ok" {
msg := fmt.Sprintf( msg := fmt.Sprintf(
"Host: %s\nIP: %s\nError: %s", "Host: %s\nIP: %s\nError: %s",
hostname, ip, err, hostname, ip, err,
@@ -727,7 +721,7 @@ func (w *Watcher) handleTLSError(
w.state.SetCertificateState( w.state.SetCertificateState(
certKey, &state.CertificateState{ certKey, &state.CertificateState{
Status: statusError, Status: "error",
Error: err.Error(), Error: err.Error(),
LastChecked: now, LastChecked: now,
}, },
@@ -754,7 +748,7 @@ func (w *Watcher) handleTLSSuccess(
Issuer: cert.Issuer, Issuer: cert.Issuer,
NotAfter: cert.NotAfter, NotAfter: cert.NotAfter,
SubjectAlternativeNames: cert.SubjectAlternativeNames, SubjectAlternativeNames: cert.SubjectAlternativeNames,
Status: statusOK, Status: "ok",
LastChecked: now, LastChecked: now,
}, },
) )
@@ -766,7 +760,7 @@ func (w *Watcher) detectTLSChanges(
prev *state.CertificateState, prev *state.CertificateState,
cert *tlscheck.CertificateInfo, cert *tlscheck.CertificateInfo,
) { ) {
if prev.Status == statusError { if prev.Status == "error" {
msg := fmt.Sprintf( msg := fmt.Sprintf(
"Host: %s\nIP: %s\nTLS recovered", "Host: %s\nIP: %s\nTLS recovered",
hostname, ip, hostname, ip,

View File

@@ -9,9 +9,9 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Pinned versions, 2026-08-07 (same pins as the Dockerfile) # Pinned versions, 2026-07-07 (same pins as the Dockerfile)
# golangci-lint v2.12.2 # golangci-lint v2.10.1
GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@c0d3ddc9cf3faa61a4e378e879ece580256d76e5" GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@5d1e709b7be35cb2025444e19de266b056b7b7ee"
# goimports v0.42.0 # goimports v0.42.0
GOIMPORTS_REF="golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0" GOIMPORTS_REF="golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0"