tests: remove the DNS stand-ins from the watcher and resolver tests (closes #159)
check / check (push) Successful in 1m11s
check / check (push) Successful in 1m11s
The watcher tests used a stand-in resolver and the resolver timeout test a stand-in DNS client, against the rule that DNS is never mocked. Watcher tests that look something up in DNS now run the real resolver against live servers, each attempt on a new watcher. A DNS change is tested by saving values live DNS never returns (names under .invalid, 192.0.2.1) in the state a check starts from, or by marking a real nameserver failed. The timeout test queries 192.0.2.1, where nothing answers. The live-DNS retry and concurrency limit moved from the resolver tests to internal/livedns, so both packages share them. NewFromLoggerWithClient had no other use and is gone. TESTING.md now states the README's rule. Model: opus-5-5
This commit is contained in:
+257
-486
@@ -2,151 +2,77 @@ package watcher_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/dnswatcher/internal/config"
|
||||
"sneak.berlin/go/dnswatcher/internal/livedns"
|
||||
"sneak.berlin/go/dnswatcher/internal/portcheck"
|
||||
"sneak.berlin/go/dnswatcher/internal/resolver"
|
||||
"sneak.berlin/go/dnswatcher/internal/state"
|
||||
"sneak.berlin/go/dnswatcher/internal/tlscheck"
|
||||
"sneak.berlin/go/dnswatcher/internal/watcher"
|
||||
)
|
||||
|
||||
// errNotFound is returned when mock data is missing.
|
||||
var errNotFound = errors.New("not found")
|
||||
|
||||
// Fixture values shared across tests.
|
||||
// The watcher looks these names up in live DNS with the real resolver,
|
||||
// so tests assert on what the watcher does with the answers, never on
|
||||
// the records these zones publish. testHost's nameservers and addresses
|
||||
// stay the same from one check to the next, which the tests that check
|
||||
// it twice rely on.
|
||||
const (
|
||||
testDomain = "example.com"
|
||||
testHost = "www.example.com"
|
||||
testNS1 = "ns1.example.com."
|
||||
testNS2 = "ns2.example.com."
|
||||
testIPv4 = "93.184.216.34"
|
||||
testIP = "1.2.3.4"
|
||||
testDomain = "google.com"
|
||||
testHost = "cloudflare.com"
|
||||
testIssuer = "DigiCert"
|
||||
)
|
||||
|
||||
// --- Mock implementations ---
|
||||
// Saved-state values that live DNS never returns: nameserver names
|
||||
// under .invalid and a documentation address.
|
||||
const (
|
||||
oldNS1 = "ns1.example.invalid."
|
||||
oldNS2 = "ns2.example.invalid."
|
||||
oldIP = "192.0.2.1"
|
||||
)
|
||||
|
||||
type mockResolver struct {
|
||||
mu sync.Mutex
|
||||
nsRecords map[string][]string
|
||||
allRecords map[string]map[string]map[string][]string
|
||||
ipAddresses map[string][]string
|
||||
lookupNSErr error
|
||||
allRecordsErr error
|
||||
resolveIPErr error
|
||||
lookupNSCalls int
|
||||
allRecordCalls int
|
||||
}
|
||||
|
||||
func (m *mockResolver) LookupNS(
|
||||
_ context.Context,
|
||||
domain string,
|
||||
) ([]string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.lookupNSCalls++
|
||||
|
||||
if m.lookupNSErr != nil {
|
||||
return nil, m.lookupNSErr
|
||||
}
|
||||
|
||||
ns, ok := m.nsRecords[domain]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: NS for %s", errNotFound, domain,
|
||||
)
|
||||
}
|
||||
|
||||
return ns, nil
|
||||
}
|
||||
|
||||
func (m *mockResolver) LookupAllRecords(
|
||||
_ context.Context,
|
||||
hostname string,
|
||||
) (map[string]map[string][]string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.allRecordCalls++
|
||||
|
||||
if m.allRecordsErr != nil {
|
||||
return nil, m.allRecordsErr
|
||||
}
|
||||
|
||||
recs, ok := m.allRecords[hostname]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: records for %s", errNotFound, hostname,
|
||||
)
|
||||
}
|
||||
|
||||
return recs, nil
|
||||
}
|
||||
|
||||
func (m *mockResolver) ResolveIPAddresses(
|
||||
_ context.Context,
|
||||
hostname string,
|
||||
) ([]string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.resolveIPErr != nil {
|
||||
return nil, m.resolveIPErr
|
||||
}
|
||||
|
||||
ips, ok := m.ipAddresses[hostname]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: IPs for %s", errNotFound, hostname,
|
||||
)
|
||||
}
|
||||
|
||||
return ips, nil
|
||||
}
|
||||
// --- Stand-ins for the port checker, TLS checker and notifier ---
|
||||
//
|
||||
// DNS has none: the watchers built here use the real resolver (see
|
||||
// TESTING.md).
|
||||
|
||||
// mockPortChecker reports every port open until closed is set.
|
||||
type mockPortChecker struct {
|
||||
mu sync.Mutex
|
||||
results map[string]bool
|
||||
err error
|
||||
calls int
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
calls int
|
||||
}
|
||||
|
||||
func (m *mockPortChecker) CheckPort(
|
||||
_ context.Context,
|
||||
address string,
|
||||
port int,
|
||||
_ string,
|
||||
_ int,
|
||||
) (*portcheck.PortResult, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.calls++
|
||||
|
||||
if m.err != nil {
|
||||
return nil, m.err
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s:%d", address, port)
|
||||
open := m.results[key]
|
||||
|
||||
return &portcheck.PortResult{Open: open}, nil
|
||||
return &portcheck.PortResult{Open: !m.closed}, nil
|
||||
}
|
||||
|
||||
// mockTLSChecker returns a certificate for the requested hostname that
|
||||
// expires at notAfter.
|
||||
type mockTLSChecker struct {
|
||||
mu sync.Mutex
|
||||
certs map[string]*tlscheck.CertificateInfo
|
||||
err error
|
||||
calls int
|
||||
mu sync.Mutex
|
||||
notAfter time.Time
|
||||
calls int
|
||||
}
|
||||
|
||||
func (m *mockTLSChecker) CheckCertificate(
|
||||
_ context.Context,
|
||||
ip string,
|
||||
_ string,
|
||||
hostname string,
|
||||
) (*tlscheck.CertificateInfo, error) {
|
||||
m.mu.Lock()
|
||||
@@ -154,20 +80,12 @@ func (m *mockTLSChecker) CheckCertificate(
|
||||
|
||||
m.calls++
|
||||
|
||||
if m.err != nil {
|
||||
return nil, m.err
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s:%s", ip, hostname)
|
||||
cert, ok := m.certs[key]
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: cert for %s", errNotFound, key,
|
||||
)
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
return &tlscheck.CertificateInfo{
|
||||
CommonName: hostname,
|
||||
Issuer: testIssuer,
|
||||
NotAfter: m.notAfter,
|
||||
SubjectAlternativeNames: []string{hostname},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type notification struct {
|
||||
@@ -205,10 +123,9 @@ func (m *mockNotifier) getNotifications() []notification {
|
||||
return result
|
||||
}
|
||||
|
||||
// --- Helper to build a Watcher for testing ---
|
||||
// --- Helpers to build a Watcher and run its checks against live DNS ---
|
||||
|
||||
type testDeps struct {
|
||||
resolver *mockResolver
|
||||
portChecker *mockPortChecker
|
||||
tlsChecker *mockTLSChecker
|
||||
notifier *mockNotifier
|
||||
@@ -223,16 +140,9 @@ func newTestWatcher(
|
||||
t.Helper()
|
||||
|
||||
deps := &testDeps{
|
||||
resolver: &mockResolver{
|
||||
nsRecords: make(map[string][]string),
|
||||
allRecords: make(map[string]map[string]map[string][]string),
|
||||
ipAddresses: make(map[string][]string),
|
||||
},
|
||||
portChecker: &mockPortChecker{
|
||||
results: make(map[string]bool),
|
||||
},
|
||||
portChecker: &mockPortChecker{},
|
||||
tlsChecker: &mockTLSChecker{
|
||||
certs: make(map[string]*tlscheck.CertificateInfo),
|
||||
notAfter: time.Now().Add(90 * 24 * time.Hour),
|
||||
},
|
||||
notifier: &mockNotifier{},
|
||||
config: cfg,
|
||||
@@ -243,7 +153,7 @@ func newTestWatcher(
|
||||
w := watcher.NewForTest(
|
||||
deps.config,
|
||||
deps.state,
|
||||
deps.resolver,
|
||||
resolver.NewFromLogger(slog.Default()),
|
||||
deps.portChecker,
|
||||
deps.tlsChecker,
|
||||
deps.notifier,
|
||||
@@ -263,6 +173,122 @@ func defaultTestConfig(t *testing.T) *config.Config {
|
||||
}
|
||||
}
|
||||
|
||||
// checkOnce runs the watcher's checks once and returns an error when a
|
||||
// configured name has no hostname state saved by this check, or that
|
||||
// state holds no address. Either live DNS gave no answer for the name,
|
||||
// or the watcher saved no fresh result for it.
|
||||
func checkOnce(
|
||||
ctx context.Context,
|
||||
w *watcher.Watcher,
|
||||
deps *testDeps,
|
||||
) error {
|
||||
started := time.Now()
|
||||
|
||||
w.RunOnce(ctx)
|
||||
|
||||
names := slices.Concat(deps.config.Domains, deps.config.Hostnames)
|
||||
|
||||
for _, name := range names {
|
||||
hs, ok := deps.state.GetHostnameState(name)
|
||||
if !ok || hs.LastChecked.Before(started) ||
|
||||
len(addresses(hs)) == 0 {
|
||||
return fmt.Errorf(
|
||||
"%s: %w, or the watcher saved no fresh "+
|
||||
"result for it",
|
||||
name, livedns.ErrNoAnswer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runChecks builds a watcher, lets prepare set up the saved state and
|
||||
// stand-ins it starts from, and runs its checks once against live DNS.
|
||||
// If change is not nil, change then alters the saved state or stand-ins
|
||||
// and the checks run a second time. When either check finds no fresh
|
||||
// address for a name (see checkOnce), the watcher is thrown away and
|
||||
// all of this runs again on a new one, so a failed attempt leaves
|
||||
// nothing behind in the saved state, the stand-ins or the notifications.
|
||||
func runChecks(
|
||||
t *testing.T,
|
||||
cfg *config.Config,
|
||||
prepare, change func(deps *testDeps),
|
||||
) *testDeps {
|
||||
t.Helper()
|
||||
|
||||
var deps *testDeps
|
||||
|
||||
livedns.Retry(t, "watcher checks", func(ctx context.Context) error {
|
||||
var w *watcher.Watcher
|
||||
|
||||
w, deps = newTestWatcher(t, cfg)
|
||||
|
||||
if prepare != nil {
|
||||
prepare(deps)
|
||||
}
|
||||
|
||||
err := checkOnce(ctx, w, deps)
|
||||
if err != nil || change == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
change(deps)
|
||||
|
||||
return checkOnce(ctx, w, deps)
|
||||
})
|
||||
|
||||
return deps
|
||||
}
|
||||
|
||||
// addresses returns the A and AAAA values saved for a hostname.
|
||||
func addresses(hs *state.HostnameState) []string {
|
||||
var ips []string
|
||||
|
||||
for _, nsState := range hs.RecordsByNameserver {
|
||||
ips = append(ips, nsState.Records["A"]...)
|
||||
ips = append(ips, nsState.Records["AAAA"]...)
|
||||
}
|
||||
|
||||
return ips
|
||||
}
|
||||
|
||||
// assertNotified checks that a notification with this title and
|
||||
// priority was sent.
|
||||
func assertNotified(
|
||||
t *testing.T,
|
||||
deps *testDeps,
|
||||
title, priority string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
notifications := deps.notifier.getNotifications()
|
||||
|
||||
for _, n := range notifications {
|
||||
if n.Title == title && n.Priority == priority {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t.Errorf(
|
||||
"expected %s notification %q, got: %v",
|
||||
priority, title, notifications,
|
||||
)
|
||||
}
|
||||
|
||||
// countNotifications counts the notifications sent with this title.
|
||||
func countNotifications(deps *testDeps, title string) int {
|
||||
count := 0
|
||||
|
||||
for _, n := range deps.notifier.getNotifications() {
|
||||
if n.Title == title {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
func TestFirstRunBaseline(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -270,51 +296,12 @@ func TestFirstRunBaseline(t *testing.T) {
|
||||
cfg.Domains = []string{testDomain}
|
||||
cfg.Hostnames = []string{testHost}
|
||||
|
||||
w, deps := newTestWatcher(t, cfg)
|
||||
setupBaselineMocks(deps)
|
||||
|
||||
w.RunOnce(t.Context())
|
||||
deps := runChecks(t, cfg, nil, nil)
|
||||
|
||||
assertNoNotifications(t, deps)
|
||||
assertStatePopulated(t, deps)
|
||||
}
|
||||
|
||||
func setupBaselineMocks(deps *testDeps) {
|
||||
deps.resolver.nsRecords[testDomain] = []string{
|
||||
testNS1,
|
||||
testNS2,
|
||||
}
|
||||
deps.resolver.allRecords[testDomain] = map[string]map[string][]string{
|
||||
testNS1: {"A": {testIPv4}},
|
||||
testNS2: {"A": {testIPv4}},
|
||||
}
|
||||
deps.resolver.allRecords[testHost] = map[string]map[string][]string{
|
||||
testNS1: {"A": {testIPv4}},
|
||||
testNS2: {"A": {testIPv4}},
|
||||
}
|
||||
deps.resolver.ipAddresses[testHost] = []string{
|
||||
testIPv4,
|
||||
}
|
||||
deps.portChecker.results["93.184.216.34:80"] = true
|
||||
deps.portChecker.results["93.184.216.34:443"] = true
|
||||
deps.tlsChecker.certs["93.184.216.34:www.example.com"] = &tlscheck.CertificateInfo{
|
||||
CommonName: testHost,
|
||||
Issuer: testIssuer,
|
||||
NotAfter: time.Now().Add(90 * 24 * time.Hour),
|
||||
SubjectAlternativeNames: []string{
|
||||
testHost,
|
||||
},
|
||||
}
|
||||
deps.tlsChecker.certs["93.184.216.34:example.com"] = &tlscheck.CertificateInfo{
|
||||
CommonName: testDomain,
|
||||
Issuer: testIssuer,
|
||||
NotAfter: time.Now().Add(90 * 24 * time.Hour),
|
||||
SubjectAlternativeNames: []string{
|
||||
testDomain,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func assertNoNotifications(
|
||||
t *testing.T,
|
||||
deps *testDeps,
|
||||
@@ -361,26 +348,7 @@ func TestDomainPortAndTLSChecks(t *testing.T) {
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Domains = []string{testDomain}
|
||||
|
||||
w, deps := newTestWatcher(t, cfg)
|
||||
|
||||
deps.resolver.nsRecords[testDomain] = []string{
|
||||
testNS1,
|
||||
}
|
||||
deps.resolver.allRecords[testDomain] = map[string]map[string][]string{
|
||||
testNS1: {"A": {testIPv4}},
|
||||
}
|
||||
deps.portChecker.results["93.184.216.34:80"] = true
|
||||
deps.portChecker.results["93.184.216.34:443"] = true
|
||||
deps.tlsChecker.certs["93.184.216.34:example.com"] = &tlscheck.CertificateInfo{
|
||||
CommonName: testDomain,
|
||||
Issuer: testIssuer,
|
||||
NotAfter: time.Now().Add(90 * 24 * time.Hour),
|
||||
SubjectAlternativeNames: []string{
|
||||
testDomain,
|
||||
},
|
||||
}
|
||||
|
||||
w.RunOnce(t.Context())
|
||||
deps := runChecks(t, cfg, nil, nil)
|
||||
|
||||
snap := deps.state.GetSnapshot()
|
||||
|
||||
@@ -419,50 +387,18 @@ func TestNSChangeDetection(t *testing.T) {
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Domains = []string{testDomain}
|
||||
|
||||
w, deps := newTestWatcher(t, cfg)
|
||||
// The saved state lists nameservers that live DNS does not.
|
||||
deps := runChecks(t, cfg, func(deps *testDeps) {
|
||||
deps.state.SetDomainState(testDomain, &state.DomainState{
|
||||
Nameservers: []string{oldNS1, oldNS2},
|
||||
})
|
||||
}, nil)
|
||||
|
||||
deps.resolver.nsRecords[testDomain] = []string{
|
||||
testNS1,
|
||||
testNS2,
|
||||
}
|
||||
deps.resolver.allRecords[testDomain] = map[string]map[string][]string{
|
||||
testNS1: {"A": {testIP}},
|
||||
testNS2: {"A": {testIP}},
|
||||
}
|
||||
deps.portChecker.results["1.2.3.4:80"] = false
|
||||
deps.portChecker.results["1.2.3.4:443"] = false
|
||||
assertNotified(t, deps, "NS Change: "+testDomain, "warning")
|
||||
|
||||
ctx := t.Context()
|
||||
w.RunOnce(ctx)
|
||||
|
||||
deps.resolver.mu.Lock()
|
||||
deps.resolver.nsRecords[testDomain] = []string{
|
||||
testNS1,
|
||||
"ns3.example.com.",
|
||||
}
|
||||
deps.resolver.allRecords[testDomain] = map[string]map[string][]string{
|
||||
testNS1: {"A": {testIP}},
|
||||
"ns3.example.com.": {"A": {testIP}},
|
||||
}
|
||||
deps.resolver.mu.Unlock()
|
||||
|
||||
w.RunOnce(ctx)
|
||||
|
||||
notifications := deps.notifier.getNotifications()
|
||||
if len(notifications) == 0 {
|
||||
t.Error("expected notification for NS change")
|
||||
}
|
||||
|
||||
found := false
|
||||
|
||||
for _, n := range notifications {
|
||||
if n.Priority == "warning" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("expected warning-priority NS change notification")
|
||||
ds, _ := deps.state.GetDomainState(testDomain)
|
||||
if slices.Contains(ds.Nameservers, oldNS1) {
|
||||
t.Errorf("saved nameservers not updated: %v", ds.Nameservers)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -472,40 +408,18 @@ func TestRecordChangeDetection(t *testing.T) {
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Hostnames = []string{testHost}
|
||||
|
||||
w, deps := newTestWatcher(t, cfg)
|
||||
// Between the checks, save for every nameserver an address live DNS
|
||||
// never returns.
|
||||
deps := runChecks(t, cfg, nil, func(deps *testDeps) {
|
||||
hs, _ := deps.state.GetHostnameState(testHost)
|
||||
for _, nsState := range hs.RecordsByNameserver {
|
||||
nsState.Records = map[string][]string{"A": {oldIP}}
|
||||
}
|
||||
|
||||
deps.resolver.allRecords[testHost] = map[string]map[string][]string{
|
||||
testNS1: {"A": {testIPv4}},
|
||||
}
|
||||
deps.resolver.ipAddresses[testHost] = []string{
|
||||
testIPv4,
|
||||
}
|
||||
deps.portChecker.results["93.184.216.34:80"] = false
|
||||
deps.portChecker.results["93.184.216.34:443"] = false
|
||||
deps.state.SetHostnameState(testHost, hs)
|
||||
})
|
||||
|
||||
ctx := t.Context()
|
||||
w.RunOnce(ctx)
|
||||
|
||||
deps.resolver.mu.Lock()
|
||||
deps.resolver.allRecords[testHost] = map[string]map[string][]string{
|
||||
testNS1: {"A": {"93.184.216.35"}},
|
||||
}
|
||||
deps.resolver.ipAddresses[testHost] = []string{
|
||||
"93.184.216.35",
|
||||
}
|
||||
deps.resolver.mu.Unlock()
|
||||
|
||||
deps.portChecker.mu.Lock()
|
||||
deps.portChecker.results["93.184.216.35:80"] = false
|
||||
deps.portChecker.results["93.184.216.35:443"] = false
|
||||
deps.portChecker.mu.Unlock()
|
||||
|
||||
w.RunOnce(ctx)
|
||||
|
||||
notifications := deps.notifier.getNotifications()
|
||||
if len(notifications) == 0 {
|
||||
t.Error("expected notification for record change")
|
||||
}
|
||||
assertNotified(t, deps, "Record Change: "+testHost, "warning")
|
||||
}
|
||||
|
||||
func TestPortStateChange(t *testing.T) {
|
||||
@@ -514,38 +428,23 @@ func TestPortStateChange(t *testing.T) {
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Hostnames = []string{testHost}
|
||||
|
||||
w, deps := newTestWatcher(t, cfg)
|
||||
// Between the checks, every port closes.
|
||||
deps := runChecks(t, cfg, nil, func(deps *testDeps) {
|
||||
deps.portChecker.mu.Lock()
|
||||
deps.portChecker.closed = true
|
||||
deps.portChecker.mu.Unlock()
|
||||
})
|
||||
|
||||
deps.resolver.allRecords[testHost] = map[string]map[string][]string{
|
||||
testNS1: {"A": {testIP}},
|
||||
}
|
||||
deps.resolver.ipAddresses[testHost] = []string{
|
||||
testIP,
|
||||
}
|
||||
deps.portChecker.results["1.2.3.4:80"] = true
|
||||
deps.portChecker.results["1.2.3.4:443"] = true
|
||||
deps.tlsChecker.certs["1.2.3.4:www.example.com"] = &tlscheck.CertificateInfo{
|
||||
CommonName: testHost,
|
||||
Issuer: testIssuer,
|
||||
NotAfter: time.Now().Add(90 * 24 * time.Hour),
|
||||
SubjectAlternativeNames: []string{
|
||||
testHost,
|
||||
},
|
||||
}
|
||||
hs, _ := deps.state.GetHostnameState(testHost)
|
||||
assertNotified(
|
||||
t, deps, "Port Change: "+addresses(hs)[0]+":443", "warning",
|
||||
)
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
w.RunOnce(ctx)
|
||||
|
||||
deps.portChecker.mu.Lock()
|
||||
deps.portChecker.results["1.2.3.4:443"] = false
|
||||
deps.portChecker.mu.Unlock()
|
||||
|
||||
w.RunOnce(ctx)
|
||||
|
||||
notifications := deps.notifier.getNotifications()
|
||||
if len(notifications) == 0 {
|
||||
t.Error("expected notification for port state change")
|
||||
}
|
||||
// expiresInThreeDays makes the TLS checker return certificates that
|
||||
// expire within the seven-day warning period.
|
||||
func expiresInThreeDays(deps *testDeps) {
|
||||
deps.tlsChecker.notAfter = time.Now().Add(3 * 24 * time.Hour)
|
||||
}
|
||||
|
||||
func TestTLSExpiryWarning(t *testing.T) {
|
||||
@@ -554,49 +453,9 @@ func TestTLSExpiryWarning(t *testing.T) {
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Hostnames = []string{testHost}
|
||||
|
||||
w, deps := newTestWatcher(t, cfg)
|
||||
deps := runChecks(t, cfg, expiresInThreeDays, nil)
|
||||
|
||||
deps.resolver.allRecords[testHost] = map[string]map[string][]string{
|
||||
testNS1: {"A": {testIP}},
|
||||
}
|
||||
deps.resolver.ipAddresses[testHost] = []string{
|
||||
testIP,
|
||||
}
|
||||
deps.portChecker.results["1.2.3.4:80"] = true
|
||||
deps.portChecker.results["1.2.3.4:443"] = true
|
||||
deps.tlsChecker.certs["1.2.3.4:www.example.com"] = &tlscheck.CertificateInfo{
|
||||
CommonName: testHost,
|
||||
Issuer: testIssuer,
|
||||
NotAfter: time.Now().Add(3 * 24 * time.Hour),
|
||||
SubjectAlternativeNames: []string{
|
||||
testHost,
|
||||
},
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
// First run = baseline
|
||||
w.RunOnce(ctx)
|
||||
|
||||
// Second run should warn about expiry
|
||||
w.RunOnce(ctx)
|
||||
|
||||
notifications := deps.notifier.getNotifications()
|
||||
|
||||
found := false
|
||||
|
||||
for _, n := range notifications {
|
||||
if n.Priority == "warning" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf(
|
||||
"expected expiry warning, got: %v",
|
||||
notifications,
|
||||
)
|
||||
}
|
||||
assertNotified(t, deps, "TLS Expiry Warning: "+testHost, "warning")
|
||||
}
|
||||
|
||||
func TestTLSExpiryWarningDedup(t *testing.T) {
|
||||
@@ -606,50 +465,25 @@ func TestTLSExpiryWarningDedup(t *testing.T) {
|
||||
cfg.Hostnames = []string{testHost}
|
||||
cfg.TLSInterval = 24 * time.Hour
|
||||
|
||||
w, deps := newTestWatcher(t, cfg)
|
||||
title := "TLS Expiry Warning: " + testHost
|
||||
|
||||
deps.resolver.allRecords[testHost] = map[string]map[string][]string{
|
||||
testNS1: {"A": {testIP}},
|
||||
}
|
||||
deps.resolver.ipAddresses[testHost] = []string{
|
||||
testIP,
|
||||
}
|
||||
deps.portChecker.results["1.2.3.4:80"] = true
|
||||
deps.portChecker.results["1.2.3.4:443"] = true
|
||||
deps.tlsChecker.certs["1.2.3.4:www.example.com"] = &tlscheck.CertificateInfo{
|
||||
CommonName: testHost,
|
||||
Issuer: testIssuer,
|
||||
NotAfter: time.Now().Add(3 * 24 * time.Hour),
|
||||
SubjectAlternativeNames: []string{
|
||||
testHost,
|
||||
},
|
||||
// The second check comes within the TLS interval of the first,
|
||||
// so it must not warn again.
|
||||
var warnings int
|
||||
|
||||
deps := runChecks(t, cfg, expiresInThreeDays, func(deps *testDeps) {
|
||||
warnings = countNotifications(deps, title)
|
||||
})
|
||||
|
||||
if warnings == 0 {
|
||||
t.Fatal("expected expiry warnings from the first check")
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
|
||||
// First run = baseline, no notifications
|
||||
w.RunOnce(ctx)
|
||||
|
||||
// Second run should fire one expiry warning
|
||||
w.RunOnce(ctx)
|
||||
|
||||
// Third run should NOT fire another warning (dedup)
|
||||
w.RunOnce(ctx)
|
||||
|
||||
notifications := deps.notifier.getNotifications()
|
||||
|
||||
expiryCount := 0
|
||||
|
||||
for _, n := range notifications {
|
||||
if n.Title == "TLS Expiry Warning: www.example.com" {
|
||||
expiryCount++
|
||||
}
|
||||
}
|
||||
|
||||
if expiryCount != 1 {
|
||||
got := countNotifications(deps, title)
|
||||
if got != warnings {
|
||||
t.Errorf(
|
||||
"expected exactly 1 expiry warning (dedup), got %d",
|
||||
expiryCount,
|
||||
"expected %d expiry warnings (dedup), got %d",
|
||||
warnings, got,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -657,21 +491,12 @@ func TestTLSExpiryWarningDedup(t *testing.T) {
|
||||
func TestGracefulShutdown(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// No domains or hostnames: stopping does not involve DNS.
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Domains = []string{testDomain}
|
||||
cfg.DNSInterval = 100 * time.Millisecond
|
||||
cfg.TLSInterval = 100 * time.Millisecond
|
||||
|
||||
w, deps := newTestWatcher(t, cfg)
|
||||
|
||||
deps.resolver.nsRecords[testDomain] = []string{
|
||||
testNS1,
|
||||
}
|
||||
deps.resolver.allRecords[testDomain] = map[string]map[string][]string{
|
||||
testNS1: {"A": {testIP}},
|
||||
}
|
||||
deps.portChecker.results["1.2.3.4:80"] = false
|
||||
deps.portChecker.results["1.2.3.4:443"] = false
|
||||
w, _ := newTestWatcher(t, cfg)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
@@ -693,90 +518,52 @@ func TestGracefulShutdown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func setupHostnameIP(
|
||||
deps *testDeps,
|
||||
hostname, ip string,
|
||||
) {
|
||||
deps.resolver.allRecords[hostname] = map[string]map[string][]string{
|
||||
testNS1: {"A": {ip}},
|
||||
}
|
||||
deps.portChecker.results[ip+":80"] = true
|
||||
deps.portChecker.results[ip+":443"] = true
|
||||
deps.tlsChecker.certs[ip+":"+hostname] = &tlscheck.CertificateInfo{
|
||||
CommonName: hostname,
|
||||
Issuer: testIssuer,
|
||||
NotAfter: time.Now().Add(90 * 24 * time.Hour),
|
||||
SubjectAlternativeNames: []string{hostname},
|
||||
}
|
||||
}
|
||||
|
||||
func updateHostnameIP(deps *testDeps, hostname, ip string) {
|
||||
deps.resolver.mu.Lock()
|
||||
deps.resolver.allRecords[hostname] = map[string]map[string][]string{
|
||||
testNS1: {"A": {ip}},
|
||||
}
|
||||
deps.resolver.mu.Unlock()
|
||||
|
||||
deps.portChecker.mu.Lock()
|
||||
deps.portChecker.results[ip+":80"] = true
|
||||
deps.portChecker.results[ip+":443"] = true
|
||||
deps.portChecker.mu.Unlock()
|
||||
|
||||
deps.tlsChecker.mu.Lock()
|
||||
deps.tlsChecker.certs[ip+":"+hostname] = &tlscheck.CertificateInfo{
|
||||
CommonName: hostname,
|
||||
Issuer: testIssuer,
|
||||
NotAfter: time.Now().Add(90 * 24 * time.Hour),
|
||||
SubjectAlternativeNames: []string{hostname},
|
||||
}
|
||||
deps.tlsChecker.mu.Unlock()
|
||||
}
|
||||
|
||||
func TestDNSRunsBeforePortAndTLSChecks(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Hostnames = []string{testHost}
|
||||
|
||||
w, deps := newTestWatcher(t, cfg)
|
||||
|
||||
setupHostnameIP(deps, testHost, "10.0.0.1")
|
||||
|
||||
ctx := t.Context()
|
||||
w.RunOnce(ctx)
|
||||
// The saved state says the last check found testHost at oldIP.
|
||||
deps := runChecks(t, cfg, func(deps *testDeps) {
|
||||
deps.state.SetHostnameState(testHost, &state.HostnameState{
|
||||
RecordsByNameserver: map[string]*state.NameserverRecordState{
|
||||
oldNS1: {
|
||||
Records: map[string][]string{"A": {oldIP}},
|
||||
Status: "ok",
|
||||
},
|
||||
},
|
||||
})
|
||||
}, nil)
|
||||
|
||||
snap := deps.state.GetSnapshot()
|
||||
if _, ok := snap.Ports["10.0.0.1:80"]; !ok {
|
||||
t.Fatal("expected port state for 10.0.0.1:80")
|
||||
|
||||
if _, ok := snap.Ports[oldIP+":80"]; ok {
|
||||
t.Error("port check used stale DNS: found " + oldIP + ":80")
|
||||
}
|
||||
|
||||
// DNS changes to a new IP; port and TLS must pick it up.
|
||||
updateHostnameIP(deps, testHost, "10.0.0.2")
|
||||
// Port and TLS checks must use the addresses this check found.
|
||||
for _, ip := range addresses(snap.Hostnames[testHost]) {
|
||||
if _, ok := snap.Ports[ip+":80"]; !ok {
|
||||
t.Error("port check used stale DNS: missing " + ip + ":80")
|
||||
}
|
||||
|
||||
w.RunOnce(ctx)
|
||||
|
||||
snap = deps.state.GetSnapshot()
|
||||
|
||||
if _, ok := snap.Ports["10.0.0.2:80"]; !ok {
|
||||
t.Error("port check used stale DNS: missing 10.0.0.2:80")
|
||||
}
|
||||
|
||||
certKey := "10.0.0.2:443:www.example.com"
|
||||
if _, ok := snap.Certificates[certKey]; !ok {
|
||||
t.Error("TLS check used stale DNS: missing " + certKey)
|
||||
certKey := ip + ":443:" + testHost
|
||||
if _, ok := snap.Certificates[certKey]; !ok {
|
||||
t.Error("TLS check used stale DNS: missing " + certKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendTestNotification_Enabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// No domains or hostnames: the startup notification does not
|
||||
// involve DNS.
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Domains = []string{testDomain}
|
||||
cfg.Hostnames = []string{testHost}
|
||||
cfg.SendTestNotification = true
|
||||
|
||||
w, deps := newTestWatcher(t, cfg)
|
||||
setupBaselineMocks(deps)
|
||||
|
||||
w.RunOnce(t.Context())
|
||||
|
||||
@@ -797,14 +584,11 @@ func TestSendTestNotification_ViaRun(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Domains = []string{testDomain}
|
||||
cfg.Hostnames = []string{testHost}
|
||||
cfg.SendTestNotification = true
|
||||
cfg.DNSInterval = 24 * time.Hour
|
||||
cfg.TLSInterval = 24 * time.Hour
|
||||
|
||||
w, deps := newTestWatcher(t, cfg)
|
||||
setupBaselineMocks(deps)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
@@ -844,14 +628,11 @@ func TestSendTestNotification_Disabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Domains = []string{testDomain}
|
||||
cfg.Hostnames = []string{testHost}
|
||||
cfg.SendTestNotification = false
|
||||
cfg.DNSInterval = 24 * time.Hour
|
||||
cfg.TLSInterval = 24 * time.Hour
|
||||
|
||||
w, deps := newTestWatcher(t, cfg)
|
||||
setupBaselineMocks(deps)
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
@@ -884,32 +665,22 @@ func TestNSFailureAndRecovery(t *testing.T) {
|
||||
cfg := defaultTestConfig(t)
|
||||
cfg.Hostnames = []string{testHost}
|
||||
|
||||
w, deps := newTestWatcher(t, cfg)
|
||||
// Between the checks, save every nameserver the first check found
|
||||
// as failed, and add, as answering, one that live DNS does not list.
|
||||
deps := runChecks(t, cfg, nil, func(deps *testDeps) {
|
||||
hs, _ := deps.state.GetHostnameState(testHost)
|
||||
for _, nsState := range hs.RecordsByNameserver {
|
||||
nsState.Status = "error"
|
||||
}
|
||||
|
||||
deps.resolver.allRecords[testHost] = map[string]map[string][]string{
|
||||
testNS1: {"A": {testIP}},
|
||||
testNS2: {"A": {testIP}},
|
||||
}
|
||||
deps.resolver.ipAddresses[testHost] = []string{
|
||||
testIP,
|
||||
}
|
||||
deps.portChecker.results["1.2.3.4:80"] = false
|
||||
deps.portChecker.results["1.2.3.4:443"] = false
|
||||
hs.RecordsByNameserver[oldNS1] = &state.NameserverRecordState{
|
||||
Records: map[string][]string{"A": {oldIP}},
|
||||
Status: "ok",
|
||||
}
|
||||
|
||||
ctx := t.Context()
|
||||
deps.state.SetHostnameState(testHost, hs)
|
||||
})
|
||||
|
||||
w.RunOnce(ctx)
|
||||
|
||||
deps.resolver.mu.Lock()
|
||||
deps.resolver.allRecords[testHost] = map[string]map[string][]string{
|
||||
testNS1: {"A": {testIP}},
|
||||
}
|
||||
deps.resolver.mu.Unlock()
|
||||
|
||||
w.RunOnce(ctx)
|
||||
|
||||
notifications := deps.notifier.getNotifications()
|
||||
if len(notifications) == 0 {
|
||||
t.Error("expected notification for NS disappearance")
|
||||
}
|
||||
assertNotified(t, deps, "NS Failure: "+testHost, "error")
|
||||
assertNotified(t, deps, "NS Recovery: "+testHost, "success")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user