tests: move the test-only constructors into export_test.go (closes #111)
check / check (push) Successful in 1m9s

state.NewForTest, state.NewForTestWithDataDir and watcher.NewForTest
were in ordinary source files, so they were built into the binary.
NewForTestWithDataDir and watcher.NewForTest now live in their
package's export_test.go. Tests in other packages cannot see those
files, so the watcher and middleware tests build their State with
state.New and a temporary data directory; the watcher tests no longer
try to save to /state.json. state.NewForTest, whose State saved to /,
is deleted: the state tests that used it pass t.TempDir() to
NewForTestWithDataDir, and the test of the helper itself is gone.

Model: opus-5-5
This commit is contained in:
2026-10-01 17:59:27 +00:00
parent 651429137f
commit 07f1cfa9af
8 changed files with 88 additions and 98 deletions
+2
View File
@@ -19,6 +19,8 @@ Rationale, Design, TODO, License, Author) if any are still missing.
# Completed Steps
- 2026-10-01: the test-only constructors moved to `export_test.go`, out of the
binary; no test builds a `State` that saves to `/` any more (closes #111).
- 2026-09-29: the live-DNS test package is renamed `internal/livednstest` and
added to the `test-support` `deny` list in `.golangci.yml`, so `make lint`
fails when program code imports it (closes #164).
+9 -1
View File
@@ -276,10 +276,18 @@ func newTestHandlers(t *testing.T) *handlers.Handlers {
t.Fatalf("notify.New: %v", err)
}
st, err := state.New(fxtest.NewLifecycle(t), state.Params{
Logger: log,
Config: &config.Config{DataDir: t.TempDir()},
})
if err != nil {
t.Fatalf("state.New: %v", err)
}
hnd, err := handlers.New(nil, handlers.Params{
Logger: log,
Globals: glob,
State: state.NewForTest(),
State: st,
Notify: notifier,
})
if err != nil {
+23
View File
@@ -0,0 +1,23 @@
package state
import (
"log/slog"
"sneak.berlin/go/dnswatcher/internal/config"
)
// NewForTestWithDataDir creates an empty State that saves to dataDir,
// without the fx lifecycle.
func NewForTestWithDataDir(dataDir string) *State {
return &State{
log: slog.Default(),
snapshot: &Snapshot{
Version: stateVersion,
Domains: make(map[string]*DomainState),
Hostnames: make(map[string]*HostnameState),
Ports: make(map[string]*PortState),
Certificates: make(map[string]*CertificateState),
},
config: &config.Config{DataDir: dataDir},
}
}
+7 -36
View File
@@ -739,7 +739,7 @@ func TestPortStateUnmarshalJSON_BothFormats(t *testing.T) {
func TestGetSnapshot_ReturnsCopy(t *testing.T) {
t.Parallel()
s := state.NewForTest()
s := state.NewForTestWithDataDir(t.TempDir())
populateState(t, s)
@@ -761,7 +761,7 @@ func TestGetSnapshot_ReturnsCopy(t *testing.T) {
func TestDomainState_GetSet(t *testing.T) {
t.Parallel()
s := state.NewForTest()
s := state.NewForTestWithDataDir(t.TempDir())
// Get on missing key returns false.
_, ok := s.GetDomainState("nonexistent.com")
@@ -812,7 +812,7 @@ func TestDomainState_GetSet(t *testing.T) {
func TestHostnameState_GetSet(t *testing.T) {
t.Parallel()
s := state.NewForTest()
s := state.NewForTestWithDataDir(t.TempDir())
_, ok := s.GetHostnameState("missing.example.com")
if ok {
@@ -857,7 +857,7 @@ func TestHostnameState_GetSet(t *testing.T) {
func TestPortState_GetSetDelete(t *testing.T) {
t.Parallel()
s := state.NewForTest()
s := state.NewForTestWithDataDir(t.TempDir())
_, ok := s.GetPortState("1.2.3.4:80")
if ok {
@@ -895,7 +895,7 @@ func TestPortState_GetSetDelete(t *testing.T) {
func TestGetAllPortKeys(t *testing.T) {
t.Parallel()
s := state.NewForTest()
s := state.NewForTestWithDataDir(t.TempDir())
keys := s.GetAllPortKeys()
if len(keys) != 0 {
@@ -937,7 +937,7 @@ func TestGetAllPortKeys(t *testing.T) {
func TestCertificateState_GetSet(t *testing.T) {
t.Parallel()
s := state.NewForTest()
s := state.NewForTestWithDataDir(t.TempDir())
_, ok := s.GetCertificateState("1.2.3.4:443:www.example.com")
if ok {
@@ -1158,7 +1158,7 @@ func TestLoadPreservesExistingStateOnMissingFile(t *testing.T) {
func TestConcurrentGetSet(t *testing.T) {
t.Parallel()
s := state.NewForTest()
s := state.NewForTestWithDataDir(t.TempDir())
const goroutines = 20
@@ -1368,35 +1368,6 @@ func TestMultipleSavesOverwrite(t *testing.T) {
}
}
// TestNewForTest verifies the test helper creates a valid empty state.
func TestNewForTest(t *testing.T) {
t.Parallel()
s := state.NewForTest()
snap := s.GetSnapshot()
if snap.Version != 1 {
t.Errorf("version: got %d, want 1", snap.Version)
}
if snap.Domains == nil {
t.Error("Domains map should be initialized")
}
if snap.Hostnames == nil {
t.Error("Hostnames map should be initialized")
}
if snap.Ports == nil {
t.Error("Ports map should be initialized")
}
if snap.Certificates == nil {
t.Error("Certificates map should be initialized")
}
}
// TestSaveFilePermissions verifies the saved file has restricted permissions.
func TestSaveFilePermissions(t *testing.T) {
t.Parallel()
-38
View File
@@ -1,38 +0,0 @@
package state
import (
"log/slog"
"sneak.berlin/go/dnswatcher/internal/config"
)
// NewForTest creates a State for unit testing with no persistence.
func NewForTest() *State {
return &State{
log: slog.Default(),
snapshot: &Snapshot{
Version: stateVersion,
Domains: make(map[string]*DomainState),
Hostnames: make(map[string]*HostnameState),
Ports: make(map[string]*PortState),
Certificates: make(map[string]*CertificateState),
},
config: &config.Config{DataDir: ""},
}
}
// NewForTestWithDataDir creates a State backed by the given directory
// for tests that need file persistence.
func NewForTestWithDataDir(dataDir string) *State {
return &State{
log: slog.Default(),
snapshot: &Snapshot{
Version: stateVersion,
Domains: make(map[string]*DomainState),
Hostnames: make(map[string]*HostnameState),
Ports: make(map[string]*PortState),
Certificates: make(map[string]*CertificateState),
},
config: &config.Config{DataDir: dataDir},
}
}
+25
View File
@@ -2,10 +2,35 @@ package watcher
import (
"context"
"log/slog"
"time"
"sneak.berlin/go/dnswatcher/internal/config"
"sneak.berlin/go/dnswatcher/internal/state"
)
// NewForTest creates a Watcher without fx for unit testing.
func NewForTest(
cfg *config.Config,
st *state.State,
res DNSResolver,
pc PortChecker,
tc TLSChecker,
n Notifier,
) *Watcher {
return &Watcher{
log: slog.Default(),
config: cfg,
state: st,
resolver: res,
portCheck: pc,
tlsCheck: tc,
notify: n,
firstRun: true,
expiryNotified: make(map[string]time.Time),
}
}
// NewlyDisagreeingPairs exports newlyDisagreeingPairs for testing.
func NewlyDisagreeingPairs(
prev *state.HostnameState,
-22
View File
@@ -102,28 +102,6 @@ func New(
return w, nil
}
// NewForTest creates a Watcher without fx for unit testing.
func NewForTest(
cfg *config.Config,
st *state.State,
res DNSResolver,
pc PortChecker,
tc TLSChecker,
n Notifier,
) *Watcher {
return &Watcher{
log: slog.Default(),
config: cfg,
state: st,
resolver: res,
portCheck: pc,
tlsCheck: tc,
notify: n,
firstRun: true,
expiryNotified: make(map[string]time.Time),
}
}
// Run starts the monitoring loop with periodic scheduling.
func (w *Watcher) Run(ctx context.Context) {
w.log.Info(
+22 -1
View File
@@ -9,8 +9,12 @@ import (
"testing"
"time"
"go.uber.org/fx/fxtest"
"sneak.berlin/go/dnswatcher/internal/config"
"sneak.berlin/go/dnswatcher/internal/globals"
"sneak.berlin/go/dnswatcher/internal/livednstest"
"sneak.berlin/go/dnswatcher/internal/logger"
"sneak.berlin/go/dnswatcher/internal/portcheck"
"sneak.berlin/go/dnswatcher/internal/resolver"
"sneak.berlin/go/dnswatcher/internal/state"
@@ -148,7 +152,24 @@ func newTestWatcher(
config: cfg,
}
deps.state = state.NewForTest()
g, err := globals.New(nil)
if err != nil {
t.Fatalf("globals.New: %v", err)
}
log, err := logger.New(nil, logger.Params{Globals: g})
if err != nil {
t.Fatalf("logger.New: %v", err)
}
// The watcher saves state after every check, into cfg.DataDir.
deps.state, err = state.New(fxtest.NewLifecycle(t), state.Params{
Logger: log,
Config: cfg,
})
if err != nil {
t.Fatalf("state.New: %v", err)
}
w := watcher.NewForTest(
deps.config,