All checks were successful
check / check (push) Successful in 33s
Pin golangci-lint to commit c0d3ddc9cf3faa61a4e378e879ece580256d76e5 (v2.12.2) in Dockerfile and script/bootstrap. Set .golangci.yml to the org-standard v2-schema config already used across the org's repos (owner-authorized; the same file is being landed as canonical via a prompts-repo PR). Lint settings live under linters.settings, so the lll, funlen, cyclop, and dupl thresholds are actually applied. The informational gomodguard deprecation warning this config can emit under v2.12 is accepted. 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
916 lines
19 KiB
Go
916 lines
19 KiB
Go
package watcher_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/config"
|
|
"sneak.berlin/go/dnswatcher/internal/portcheck"
|
|
"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.
|
|
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"
|
|
testIssuer = "DigiCert"
|
|
)
|
|
|
|
// --- Mock implementations ---
|
|
|
|
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
|
|
}
|
|
|
|
type mockPortChecker struct {
|
|
mu sync.Mutex
|
|
results map[string]bool
|
|
err error
|
|
calls int
|
|
}
|
|
|
|
func (m *mockPortChecker) CheckPort(
|
|
_ context.Context,
|
|
address string,
|
|
port 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
|
|
}
|
|
|
|
type mockTLSChecker struct {
|
|
mu sync.Mutex
|
|
certs map[string]*tlscheck.CertificateInfo
|
|
err error
|
|
calls int
|
|
}
|
|
|
|
func (m *mockTLSChecker) CheckCertificate(
|
|
_ context.Context,
|
|
ip string,
|
|
hostname string,
|
|
) (*tlscheck.CertificateInfo, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
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
|
|
}
|
|
|
|
type notification struct {
|
|
Title string
|
|
Message string
|
|
Priority string
|
|
}
|
|
|
|
type mockNotifier struct {
|
|
mu sync.Mutex
|
|
notifications []notification
|
|
}
|
|
|
|
func (m *mockNotifier) SendNotification(
|
|
_ context.Context,
|
|
title, message, priority string,
|
|
) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
m.notifications = append(m.notifications, notification{
|
|
Title: title,
|
|
Message: message,
|
|
Priority: priority,
|
|
})
|
|
}
|
|
|
|
func (m *mockNotifier) getNotifications() []notification {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
result := make([]notification, len(m.notifications))
|
|
copy(result, m.notifications)
|
|
|
|
return result
|
|
}
|
|
|
|
// --- Helper to build a Watcher for testing ---
|
|
|
|
type testDeps struct {
|
|
resolver *mockResolver
|
|
portChecker *mockPortChecker
|
|
tlsChecker *mockTLSChecker
|
|
notifier *mockNotifier
|
|
state *state.State
|
|
config *config.Config
|
|
}
|
|
|
|
func newTestWatcher(
|
|
t *testing.T,
|
|
cfg *config.Config,
|
|
) (*watcher.Watcher, *testDeps) {
|
|
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),
|
|
},
|
|
tlsChecker: &mockTLSChecker{
|
|
certs: make(map[string]*tlscheck.CertificateInfo),
|
|
},
|
|
notifier: &mockNotifier{},
|
|
config: cfg,
|
|
}
|
|
|
|
deps.state = state.NewForTest()
|
|
|
|
w := watcher.NewForTest(
|
|
deps.config,
|
|
deps.state,
|
|
deps.resolver,
|
|
deps.portChecker,
|
|
deps.tlsChecker,
|
|
deps.notifier,
|
|
)
|
|
|
|
return w, deps
|
|
}
|
|
|
|
func defaultTestConfig(t *testing.T) *config.Config {
|
|
t.Helper()
|
|
|
|
return &config.Config{
|
|
DNSInterval: time.Hour,
|
|
TLSInterval: 12 * time.Hour,
|
|
TLSExpiryWarning: 7,
|
|
DataDir: t.TempDir(),
|
|
}
|
|
}
|
|
|
|
func TestFirstRunBaseline(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := defaultTestConfig(t)
|
|
cfg.Domains = []string{testDomain}
|
|
cfg.Hostnames = []string{testHost}
|
|
|
|
w, deps := newTestWatcher(t, cfg)
|
|
setupBaselineMocks(deps)
|
|
|
|
w.RunOnce(t.Context())
|
|
|
|
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,
|
|
) {
|
|
t.Helper()
|
|
|
|
notifications := deps.notifier.getNotifications()
|
|
if len(notifications) != 0 {
|
|
t.Errorf(
|
|
"expected 0 notifications on first run, got %d",
|
|
len(notifications),
|
|
)
|
|
}
|
|
}
|
|
|
|
func assertStatePopulated(
|
|
t *testing.T,
|
|
deps *testDeps,
|
|
) {
|
|
t.Helper()
|
|
|
|
snap := deps.state.GetSnapshot()
|
|
|
|
if len(snap.Domains) != 1 {
|
|
t.Errorf(
|
|
"expected 1 domain in state, got %d",
|
|
len(snap.Domains),
|
|
)
|
|
}
|
|
|
|
// Hostnames includes both explicit hostnames and domains
|
|
// (domains now also get hostname state for port/TLS checks).
|
|
if len(snap.Hostnames) < 1 {
|
|
t.Errorf(
|
|
"expected at least 1 hostname in state, got %d",
|
|
len(snap.Hostnames),
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestDomainPortAndTLSChecks(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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())
|
|
|
|
snap := deps.state.GetSnapshot()
|
|
|
|
// Domain should have port state populated
|
|
if len(snap.Ports) == 0 {
|
|
t.Error("expected port state for domain, got none")
|
|
}
|
|
|
|
// Domain should have certificate state populated
|
|
if len(snap.Certificates) == 0 {
|
|
t.Error("expected certificate state for domain, got none")
|
|
}
|
|
|
|
// Verify port checker was actually called
|
|
deps.portChecker.mu.Lock()
|
|
calls := deps.portChecker.calls
|
|
deps.portChecker.mu.Unlock()
|
|
|
|
if calls == 0 {
|
|
t.Error("expected port checker to be called for domain")
|
|
}
|
|
|
|
// Verify TLS checker was actually called
|
|
deps.tlsChecker.mu.Lock()
|
|
tlsCalls := deps.tlsChecker.calls
|
|
deps.tlsChecker.mu.Unlock()
|
|
|
|
if tlsCalls == 0 {
|
|
t.Error("expected TLS checker to be called for domain")
|
|
}
|
|
}
|
|
|
|
func TestNSChangeDetection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := defaultTestConfig(t)
|
|
cfg.Domains = []string{testDomain}
|
|
|
|
w, deps := newTestWatcher(t, cfg)
|
|
|
|
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
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
func TestRecordChangeDetection(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := defaultTestConfig(t)
|
|
cfg.Hostnames = []string{testHost}
|
|
|
|
w, deps := newTestWatcher(t, cfg)
|
|
|
|
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
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
func TestPortStateChange(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := defaultTestConfig(t)
|
|
cfg.Hostnames = []string{testHost}
|
|
|
|
w, deps := newTestWatcher(t, cfg)
|
|
|
|
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,
|
|
},
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
func TestTLSExpiryWarning(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := defaultTestConfig(t)
|
|
cfg.Hostnames = []string{testHost}
|
|
|
|
w, deps := newTestWatcher(t, cfg)
|
|
|
|
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,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestTLSExpiryWarningDedup(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := defaultTestConfig(t)
|
|
cfg.Hostnames = []string{testHost}
|
|
cfg.TLSInterval = 24 * time.Hour
|
|
|
|
w, deps := newTestWatcher(t, cfg)
|
|
|
|
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, 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 {
|
|
t.Errorf(
|
|
"expected exactly 1 expiry warning (dedup), got %d",
|
|
expiryCount,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestGracefulShutdown(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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
|
|
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
w.Run(ctx)
|
|
close(done)
|
|
}()
|
|
|
|
time.Sleep(250 * time.Millisecond)
|
|
cancel()
|
|
|
|
select {
|
|
case <-done:
|
|
// Shut down cleanly
|
|
case <-time.After(5 * time.Second):
|
|
t.Error("watcher did not shut down within timeout")
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
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")
|
|
}
|
|
|
|
// DNS changes to a new IP; port and TLS must pick it up.
|
|
updateHostnameIP(deps, testHost, "10.0.0.2")
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestSendTestNotification_Enabled(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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())
|
|
|
|
// RunOnce does not send the test notification — it is
|
|
// sent by Run after RunOnce completes. Call the exported
|
|
// RunOnce then check that no test notification was sent
|
|
// (only Run triggers it). We test the full path via Run.
|
|
notifications := deps.notifier.getNotifications()
|
|
if len(notifications) != 0 {
|
|
t.Errorf(
|
|
"RunOnce should not send test notification, got %d",
|
|
len(notifications),
|
|
)
|
|
}
|
|
}
|
|
|
|
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())
|
|
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
w.Run(ctx)
|
|
close(done)
|
|
}()
|
|
|
|
// Wait for the initial scan and test notification.
|
|
time.Sleep(500 * time.Millisecond)
|
|
cancel()
|
|
|
|
<-done
|
|
|
|
notifications := deps.notifier.getNotifications()
|
|
|
|
found := false
|
|
|
|
for _, n := range notifications {
|
|
if n.Priority == "success" &&
|
|
n.Title == "✅ dnswatcher startup complete" {
|
|
found = true
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
t.Errorf(
|
|
"expected startup test notification, got: %v",
|
|
notifications,
|
|
)
|
|
}
|
|
}
|
|
|
|
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())
|
|
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
w.Run(ctx)
|
|
close(done)
|
|
}()
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
cancel()
|
|
|
|
<-done
|
|
|
|
notifications := deps.notifier.getNotifications()
|
|
|
|
for _, n := range notifications {
|
|
if n.Title == "✅ dnswatcher startup complete" {
|
|
t.Error(
|
|
"test notification should not be sent when disabled",
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNSFailureAndRecovery(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := defaultTestConfig(t)
|
|
cfg.Hostnames = []string{testHost}
|
|
|
|
w, deps := newTestWatcher(t, cfg)
|
|
|
|
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
|
|
|
|
ctx := t.Context()
|
|
|
|
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")
|
|
}
|
|
}
|