check / check (push) Canceled after 0s
At startup, before the first check, Run removes from the loaded state the domain, hostname and certificate entries of names no longer in DNSWATCHER_TARGETS, takes those names off each port entry's list of names and removes a port entry left with none, so the dashboard, /api/v1/status and the startup notification count only configured names. A configured domain's own records, saved as a hostname entry under its name, are kept. Nothing is notified. Each port check, next to the removal of stale port entries, now also removes the certificate entries for an address a name no longer resolves to, except while none of its nameservers answered, as port entries already were. Model: opus-5-5
225 lines
6.0 KiB
Go
225 lines
6.0 KiB
Go
package watcher_test
|
|
|
|
import (
|
|
"maps"
|
|
"slices"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/state"
|
|
"sneak.berlin/go/dnswatcher/internal/watcher"
|
|
)
|
|
|
|
// TestRemovedTargetsLeaveTheState loads a state saved while a domain
|
|
// and a hostname now removed from the configuration were still in it,
|
|
// and runs the removal that Run does before the first check. The
|
|
// removed names' domain, hostname and certificate entries are gone,
|
|
// the configured names' are kept, and nothing is notified. Nothing is
|
|
// looked up: the watcher has no resolver.
|
|
func TestRemovedTargetsLeaveTheState(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
removedDomain = "example.com"
|
|
removedHost = "www.example.com"
|
|
)
|
|
|
|
cfg := defaultTestConfig(t)
|
|
cfg.Domains = []string{domain}
|
|
cfg.Hostnames = []string{host}
|
|
|
|
deps := newTestDeps(t, cfg)
|
|
w := watcher.NewForTest(
|
|
cfg, deps.state, nil,
|
|
deps.portChecker, deps.tlsChecker, deps.notifier,
|
|
)
|
|
|
|
// The state a check of all four names saves, each name at ip1.
|
|
for _, name := range []string{domain, removedDomain} {
|
|
deps.state.SetDomainState(name, &state.DomainState{
|
|
Nameservers: []string{nsA},
|
|
})
|
|
}
|
|
|
|
for _, name := range []string{domain, host, removedDomain, removedHost} {
|
|
deps.state.SetHostnameState(name, saved(
|
|
map[string]*state.NameserverRecordState{
|
|
nsA: answered(map[string][]string{"A": {ip1}}),
|
|
},
|
|
))
|
|
deps.state.SetCertificateState(
|
|
ip1+":443:"+name, &state.CertificateState{Status: "ok"},
|
|
)
|
|
}
|
|
|
|
err := deps.state.Save()
|
|
if err != nil {
|
|
t.Fatalf("saving the state: %v", err)
|
|
}
|
|
|
|
err = deps.state.Load()
|
|
if err != nil {
|
|
t.Fatalf("loading the state: %v", err)
|
|
}
|
|
|
|
w.CleanupRemovedTargets()
|
|
|
|
snap := deps.state.GetSnapshot()
|
|
|
|
got := slices.Sorted(maps.Keys(snap.Domains))
|
|
if want := []string{domain}; !slices.Equal(got, want) {
|
|
t.Errorf("domain entries %v, want %v", got, want)
|
|
}
|
|
|
|
got = slices.Sorted(maps.Keys(snap.Hostnames))
|
|
if want := []string{domain, host}; !slices.Equal(got, want) {
|
|
t.Errorf("hostname entries %v, want %v", got, want)
|
|
}
|
|
|
|
got = slices.Sorted(maps.Keys(snap.Certificates))
|
|
if want := []string{
|
|
ip1 + ":443:" + domain, ip1 + ":443:" + host,
|
|
}; !slices.Equal(got, want) {
|
|
t.Errorf("certificate entries %v, want %v", got, want)
|
|
}
|
|
|
|
if sent := deps.notifier.getNotifications(); len(sent) != 0 {
|
|
t.Errorf("sent %v, want nothing", sent)
|
|
}
|
|
}
|
|
|
|
// TestRemovedTargetsLeaveThePortEntries loads a state whose port
|
|
// entries name a domain and a hostname now removed from the
|
|
// configuration, and runs the removal that Run does before the first
|
|
// check. The removed names are off each port entry's list of names, the
|
|
// entry only they had is gone, the entry that also names configured
|
|
// names is kept for the port checks, and nothing is notified.
|
|
func TestRemovedTargetsLeaveThePortEntries(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
removedDomain = "example.com"
|
|
removedHost = "www.example.com"
|
|
)
|
|
|
|
cfg := defaultTestConfig(t)
|
|
cfg.Domains = []string{domain}
|
|
cfg.Hostnames = []string{host}
|
|
|
|
deps := newTestDeps(t, cfg)
|
|
w := watcher.NewForTest(
|
|
cfg, deps.state, nil,
|
|
deps.portChecker, deps.tlsChecker, deps.notifier,
|
|
)
|
|
|
|
// The port 443 entries a check of all four names saves: each name
|
|
// at ip1, except the removed hostname, at ip2.
|
|
deps.state.SetPortState(ip1+":443", &state.PortState{
|
|
Open: true, Hostnames: []string{removedDomain, domain, host},
|
|
})
|
|
deps.state.SetPortState(ip2+":443", &state.PortState{
|
|
Open: true, Hostnames: []string{removedHost},
|
|
})
|
|
|
|
err := deps.state.Save()
|
|
if err != nil {
|
|
t.Fatalf("saving the state: %v", err)
|
|
}
|
|
|
|
err = deps.state.Load()
|
|
if err != nil {
|
|
t.Fatalf("loading the state: %v", err)
|
|
}
|
|
|
|
w.CleanupRemovedTargets()
|
|
|
|
if sent := deps.notifier.getNotifications(); len(sent) != 0 {
|
|
t.Errorf("sent %v, want nothing", sent)
|
|
}
|
|
|
|
snap := deps.state.GetSnapshot()
|
|
|
|
got := slices.Sorted(maps.Keys(snap.Ports))
|
|
if want := []string{ip1 + ":443"}; !slices.Equal(got, want) {
|
|
t.Fatalf("port entries %v, want %v", got, want)
|
|
}
|
|
|
|
got = snap.Ports[ip1+":443"].Hostnames
|
|
if want := []string{domain, host}; !slices.Equal(got, want) {
|
|
t.Errorf("names of port entry %s:443 %v, want %v", ip1, got, want)
|
|
}
|
|
}
|
|
|
|
// TestCertificateStateForAnAddressGone runs the port checks on hostname
|
|
// state built here for a configured hostname, with certificate entries
|
|
// saved for it at ip1, ip2 and an IPv6 address. When its nameservers
|
|
// answered with ip1 and the IPv6 address, the entry for ip2 is removed.
|
|
// When none of them answered, its addresses are not known, and every
|
|
// entry is kept. Nothing is notified, and nothing is looked up.
|
|
func TestCertificateStateForAnAddressGone(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const ip6 = "2001:db8::1"
|
|
|
|
tests := []struct {
|
|
name string
|
|
hostname *state.HostnameState
|
|
want []string
|
|
}{
|
|
{
|
|
"answered without ip2",
|
|
saved(map[string]*state.NameserverRecordState{
|
|
nsA: answered(map[string][]string{
|
|
"A": {ip1}, "AAAA": {ip6},
|
|
}),
|
|
}),
|
|
[]string{ip1 + ":443:" + host, ip6 + ":443:" + host},
|
|
},
|
|
{
|
|
"no nameserver answered",
|
|
saved(map[string]*state.NameserverRecordState{
|
|
nsA: failed(), nsB: failed(),
|
|
}),
|
|
[]string{
|
|
ip1 + ":443:" + host,
|
|
ip2 + ":443:" + host,
|
|
ip6 + ":443:" + host,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := defaultTestConfig(t)
|
|
cfg.Hostnames = []string{host}
|
|
|
|
deps := newTestDeps(t, cfg)
|
|
w := watcher.NewForTest(
|
|
cfg, deps.state, nil,
|
|
deps.portChecker, deps.tlsChecker, deps.notifier,
|
|
)
|
|
w.SetFirstRun(false)
|
|
|
|
deps.state.SetHostnameState(host, tt.hostname)
|
|
|
|
for _, ip := range []string{ip1, ip2, ip6} {
|
|
deps.state.SetCertificateState(
|
|
ip+":443:"+host, &state.CertificateState{Status: "ok"},
|
|
)
|
|
}
|
|
|
|
w.CheckAllPorts(t.Context())
|
|
|
|
got := slices.Sorted(maps.Keys(deps.state.GetSnapshot().Certificates))
|
|
if !slices.Equal(got, tt.want) {
|
|
t.Errorf("certificate entries %v, want %v", got, tt.want)
|
|
}
|
|
|
|
if sent := deps.notifier.getNotifications(); len(sent) != 0 {
|
|
t.Errorf("sent %v, want nothing", sent)
|
|
}
|
|
})
|
|
}
|
|
}
|