1 Commits

Author SHA1 Message Date
cd34e52064 build: update golangci-lint to v2.12.2 with new canonical v2 config
All checks were successful
check / check (push) Successful in 31s
Pin golangci-lint to commit c0d3ddc9cf3faa61a4e378e879ece580256d76e5
(v2.12.2) in Dockerfile and script/bootstrap.

Replace .golangci.yml with a v2-schema migration of the canonical
config, produced with golangci-lint migrate (owner-authorized; the
same file becomes the new org-wide canonical via a prompts-repo PR).
Lint settings now live under linters.settings, so the lll, funlen,
cyclop, and dupl thresholds are actually applied. The deprecated
gomodguard linter is disabled in favor of gomodguard_v2, resolving
the v2.12 deprecation warning. Deliberate delta from the migrate
output: the gci formatter is not enabled because its default two-group
import ordering conflicts with the repo's stdlib/third-party/local
import style that script/fmt (gofmt + goimports) produces.

Fix all findings surfaced by the now-active thresholds:

- goconst: shared constants for repeated status, priority, and DNS
  fixture strings in watcher.go and the notify, state, and watcher
  tests
- dupl: consolidate duplicated ntfy/slack HTTP-error tests and
  SendNotification endpoint-error tests behind shared helpers
- lll: wrap long test table entries and comments; shorten one inline
  nolint justification
2026-08-07 20:38:01 +00:00
7 changed files with 570 additions and 469 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
@@ -16,6 +12,7 @@ linters:
- depguard # Dependency allow/block lists - depguard # Dependency allow/block lists
- godot # Requires comments to end with periods - godot # Requires comments to end with periods
- wsl # Deprecated, replaced by wsl_v5 - wsl # Deprecated, replaced by wsl_v5
- gomodguard # Deprecated, replaced by gomodguard_v2
- 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: settings:
@@ -28,7 +25,25 @@ linters:
max-complexity: 15 max-complexity: 15
dupl: dupl:
threshold: 100 threshold: 100
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
issues: issues:
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
formatters:
enable:
- gofmt
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View File

@@ -2,10 +2,8 @@
## DNS Resolution Tests ## DNS Resolution Tests
All tests that involve DNS resolution — in every package, including All resolver tests **MUST** use live queries against real DNS servers.
consumers of the resolver such as the watcher — **MUST** use live No mocking of the DNS client layer is permitted.
queries against real DNS servers. No mocking, faking, or stubbing of
DNS at any layer is permitted.
### Rationale ### Rationale
@@ -14,8 +12,6 @@ the full delegation chain. Mocked responses cannot faithfully represent
the variety of real-world DNS behavior (truncation, referrals, glue the variety of real-world DNS behavior (truncation, referrals, glue
records, DNSSEC, varied response times, EDNS, etc.). Testing against records, DNSSEC, varied response times, EDNS, etc.). Testing against
real servers ensures the resolver works correctly in production. real servers ensures the resolver works correctly in production.
Robustness comes from handling real-world DNS behavior with tolerant
assertions and sensible timeouts, not from mocks.
### Constraints ### Constraints
@@ -28,14 +24,11 @@ assertions and sensible timeouts, not from mocks.
- Flaky failures from transient network issues are acceptable and - Flaky failures from transient network issues are acceptable and
should be investigated as potential resolver bugs, not papered over should be investigated as potential resolver bugs, not papered over
with mocks or skip flags with mocks or skip flags
- Watcher change-detection tests seed a synthetic *previous state*
and compare it against fresh live lookups; the DNS side is never
faked
### What NOT to do ### What NOT to do
- **Do not mock `DNSClient`**, the watcher's `DNSResolver` interface, - **Do not mock `DNSClient`** for resolver tests (the mock constructor
or any other DNS abstraction — in any package, for any reason exists for unit-testing other packages that consume the resolver)
- **Do not add `-short` flags** to skip slow tests - **Do not add `-short` flags** to skip slow tests
- **Do not increase `-timeout`** to hide hanging queries - **Do not increase `-timeout`** to hide hanging queries
- **Do not modify linter configuration** to suppress findings - **Do not modify linter configuration** to suppress findings

36
TODO.md
View File

@@ -14,10 +14,7 @@ pre-1.0. No git tags. Core resolver work in flight on feature/resolver
(dirty: internal/resolver/resolver_test.go). Local checkout has diverged (dirty: internal/resolver/resolver_test.go). Local checkout has diverged
from origin: origin/main is 8 commits ahead (watcher orchestrator, from origin: origin/main is 8 commits ahead (watcher orchestrator,
unified TARGETS) and origin/feature/resolver already contains the full unified TARGETS) and origin/feature/resolver already contains the full
iterative resolver implementation. DNS mocking is banned in this repo iterative resolver implementation with hermetic mocked tests.
(see `TESTING.md`): all tests use live DNS only. The hermetic mocked
tests previously noted on `feature/resolver` are gone from its current
tip, which carries a live-DNS suite against `*.dns.sneak.cloud`.
# Next Step # Next Step
@@ -28,23 +25,16 @@ confirm make check still passes.
# Completed Steps # Completed Steps
- 2026-08-07: DNS mocking removed from the entire test suite; watcher
tests now drive the real iterative resolver against live DNS and
`TESTING.md` bans DNS mocks in every package (`remove-dns-mocking`
branch)
- 2026-08-07: golangci-lint bumped to v2.12.2 (commit-pinned installs - 2026-08-07: golangci-lint bumped to v2.12.2 (commit-pinned installs
in `Dockerfile` and `script/bootstrap`); `.golangci.yml` set to the in `Dockerfile` and `script/bootstrap`); `.golangci.yml` migrated to
org-standard v2-schema config used across the org's repos the v2 schema (owner-authorized; becomes the new org canonical), so
(owner-authorized; same file is being landed as canonical via prompts the lll/funlen/cyclop/dupl thresholds now apply; deprecated
PR #24), with settings under `linters.settings` so the `gomodguard` disabled in favor of `gomodguard_v2`; fixed the
lll/funlen/cyclop/dupl thresholds apply; fixed the resulting resulting `goconst`, `dupl`, and `lll` findings
`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
with mocked DNS (origin/feature/resolver, unmerged; superseded — DNS with mocked DNS (origin/feature/resolver, unmerged)
mocking is banned, see `TESTING.md`)
- 2026-02-20: CI actions and go install refs pinned to commit SHAs; - 2026-02-20: CI actions and go install refs pinned to commit SHAs;
Gitea Actions workflow for make check (origin/ci/make-check, unmerged) Gitea Actions workflow for make check (origin/ci/make-check, unmerged)
- 2026-02-20: watcher monitoring orchestrator merged to main (#8) - 2026-02-20: watcher monitoring orchestrator merged to main (#8)
@@ -71,9 +61,8 @@ Branch reconciliation:
- Sync local checkout with origin: local main is 8 commits behind - Sync local checkout with origin: local main is 8 commits behind
origin/main; local feature/resolver has diverged from origin/main; local feature/resolver has diverged from
origin/feature/resolver, which already implements the resolver origin/feature/resolver, which already implements the resolver
- Merge in-flight branches to main once green: feature/resolver - Merge in-flight branches to main once green: feature/resolver,
(confirm its tests remain live-DNS — DNS mocking is banned, see ci/make-check, feature/portcheck-implementation,
`TESTING.md`), ci/make-check, feature/portcheck-implementation,
feature/tlscheck-implementation feature/tlscheck-implementation
Resolver (plan from untracked TODO.md; largely implemented on Resolver (plan from untracked TODO.md; largely implemented on
@@ -157,7 +146,6 @@ Infrastructure notes (from untracked TODO.md):
- Module path sneak.berlin/go/dnswatcher differs from the git.eeqj.de - Module path sneak.berlin/go/dnswatcher differs from the git.eeqj.de
remote intentionally; do not "fix" it remote intentionally; do not "fix" it
- Dependencies: github.com/miekg/dns, golang.org/x/net/publicsuffix - Dependencies: github.com/miekg/dns, golang.org/x/net/publicsuffix
- Resolver tests originally used live DNS against `*.dns.sneak.cloud` - Resolver tests originally used live DNS against *.dns.sneak.cloud
(required records documented in the test file header); `main` now (required records documented in the test file header); origin now has
tests against live public DNS. DNS mocking is banned (see mocked hermetic tests, keep them hermetic
`TESTING.md`); never reintroduce hermetic mocked DNS tests

View File

@@ -7,8 +7,8 @@ import (
"github.com/miekg/dns" "github.com/miekg/dns"
) )
// DNSClient abstracts DNS wire-protocol exchanges over a single // DNSClient abstracts DNS wire-protocol exchanges so the resolver
// transport, letting the resolver switch between UDP and TCP. // can be tested without hitting real nameservers.
type DNSClient interface { type DNSClient interface {
ExchangeContext( ExchangeContext(
ctx context.Context, ctx context.Context,

View File

@@ -67,4 +67,17 @@ func NewFromLogger(log *slog.Logger) *Resolver {
} }
} }
// NewFromLoggerWithClient creates a Resolver with a custom DNS
// client, useful for testing with mock DNS responses.
func NewFromLoggerWithClient(
log *slog.Logger,
client DNSClient,
) *Resolver {
return &Resolver{
log: log,
client: client,
tcp: client,
}
}
// Method implementations are in iterative.go. // Method implementations are in iterative.go.

View File

@@ -10,6 +10,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -623,41 +624,58 @@ func TestQueryAllNameservers_ContextCanceled(t *testing.T) {
} }
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// Unreachable nameserver tests // Timeout tests
// ---------------------------------------------------------------- // ----------------------------------------------------------------
func TestQueryNameserverIP_UnreachableServer(t *testing.T) { func TestQueryNameserverIP_Timeout(t *testing.T) {
t.Parallel() t.Parallel()
r := newTestResolver(t) log := slog.New(slog.NewTextHandler(
os.Stderr,
&slog.HandlerOptions{Level: slog.LevelDebug},
))
r := resolver.NewFromLoggerWithClient(
log, &timeoutClient{},
)
ctx, cancel := context.WithTimeout( ctx, cancel := context.WithTimeout(
context.Background(), 10*time.Second, context.Background(), 10*time.Second,
) )
t.Cleanup(cancel) t.Cleanup(cancel)
// 192.0.2.1 is an RFC 5737 documentation address: no // Query any IP — the client always returns a timeout error.
// nameserver can exist there. Depending on the network
// path the queries either time out (silent drop) or fail
// fast (ICMP unreachable), so accept any non-OK status;
// the resolver must return a classified response with no
// records rather than an error or a hang.
resp, err := r.QueryNameserverIP( resp, err := r.QueryNameserverIP(
ctx, "unreachable.test.", "192.0.2.1", ctx, "unreachable.test.", "192.0.2.1",
"example.com", "example.com",
) )
require.NoError(t, err) require.NoError(t, err)
assert.NotEqual(t, resolver.StatusOK, resp.Status) assert.Equal(t, resolver.StatusTimeout, resp.Status)
assert.NotEmpty(t, resp.Error)
totalRecords := 0
for _, values := range resp.Records {
totalRecords += len(values)
}
assert.Zero(t, totalRecords)
} }
// timeoutClient simulates DNS timeout errors for testing.
type timeoutClient struct{}
func (c *timeoutClient) ExchangeContext(
_ context.Context,
_ *dns.Msg,
_ string,
) (*dns.Msg, time.Duration, error) {
return nil, 0, &net.OpError{
Op: "read",
Net: "udp",
Err: &timeoutError{},
}
}
type timeoutError struct{}
func (e *timeoutError) Error() string { return "i/o timeout" }
func (e *timeoutError) Timeout() bool { return true }
func (e *timeoutError) Temporary() bool { return true }
func TestResolveIPAddresses_ContextCanceled(t *testing.T) { func TestResolveIPAddresses_ContextCanceled(t *testing.T) {
t.Parallel() t.Parallel()

File diff suppressed because it is too large Load Diff