check / check (push) Canceled after 0s
The TLS check runs only on an open port 443, and the certificate cleanup removed an entry only when its name stopped resolving to the address, so an entry stayed ok for as long as its port was closed. The cleanup after the port checks now also removes the entries of an address whose port 443 was found closed. Nothing is notified for the removal; the Port Change already says the port closed. When the port opens again, the next TLS check saves the certificate as new, with no change or recovery notification. The README says both. Judgement call: the entry goes even while none of the name's nameservers answer, since the port check reached the address itself. Model: opus-5-5
290 lines
7.9 KiB
Go
290 lines
7.9 KiB
Go
package watcher_test
|
|
|
|
import (
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
"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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCertificateStateForAPortClosed runs the port checks on hostname
|
|
// and port state built here for a configured hostname at ip1, whose
|
|
// port 443 the previous check found open and whose certificate entry
|
|
// is ok. The port is closed now: the certificate entry is removed, and
|
|
// only the Port Change is notified. When the port opens again, the next
|
|
// TLS check saves the certificate as new and notifies nothing about it.
|
|
// Nothing is looked up.
|
|
func TestCertificateStateForAPortClosed(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)
|
|
|
|
certKey := ip1 + ":443:" + host
|
|
|
|
deps.state.SetHostnameState(host, saved(
|
|
map[string]*state.NameserverRecordState{
|
|
nsA: answered(map[string][]string{"A": {ip1}}),
|
|
},
|
|
))
|
|
deps.state.SetPortState(ip1+":443", &state.PortState{
|
|
Open: true, Hostnames: []string{host},
|
|
})
|
|
deps.state.SetCertificateState(certKey, &state.CertificateState{
|
|
CommonName: host, Status: "ok",
|
|
})
|
|
deps.portChecker.closed = true
|
|
|
|
w.CheckAllPorts(t.Context())
|
|
|
|
if _, ok := deps.state.GetCertificateState(certKey); ok {
|
|
t.Errorf("certificate entry %s kept with port 443 closed", certKey)
|
|
}
|
|
|
|
sent := deps.notifier.getNotifications()
|
|
if title := "Port Change: " + ip1 + ":443"; len(sent) != 1 ||
|
|
sent[0].Title != title {
|
|
t.Errorf("sent %v, want one %q", sent, title)
|
|
}
|
|
|
|
deps.portChecker.closed = false
|
|
|
|
w.CheckAllPorts(t.Context())
|
|
w.RunTLSChecks(t.Context())
|
|
|
|
cert, ok := deps.state.GetCertificateState(certKey)
|
|
if !ok || cert.Status != "ok" {
|
|
t.Errorf("certificate entry %s after port 443 opened: %+v", certKey, cert)
|
|
}
|
|
|
|
for _, n := range deps.notifier.getNotifications() {
|
|
if strings.HasPrefix(n.Title, "TLS") {
|
|
t.Errorf("sent %v, want no TLS notification", n)
|
|
}
|
|
}
|
|
}
|