check / check (push) Successful in 1m17s
A Port Change notification's `Hosts:` line listed a port's apex domains and hostnames together. It now has a `Domains:` line and a `Hostnames:` line, and leaves out one that would name nothing. A name is a domain when it is a configured domain, the rule record notifications already use to start `Domain:` or `Hostname:`; that rule is now one method, isDomain, which both use. The port entries saved in the state are unchanged. README describes the new lines. Model: opus-5-5
216 lines
5.8 KiB
Go
216 lines
5.8 KiB
Go
package watcher_test
|
|
|
|
import (
|
|
"maps"
|
|
"strings"
|
|
"testing"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/config"
|
|
"sneak.berlin/go/dnswatcher/internal/state"
|
|
"sneak.berlin/go/dnswatcher/internal/watcher"
|
|
)
|
|
|
|
// When one nameserver's A record changes and its TXT record does not,
|
|
// the record change and the inconsistency it starts name the A record
|
|
// alone, with its values written as plain text.
|
|
func TestChangeMessagesNameTheChangedType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// A nameserver's records: this A address and the same TXT record.
|
|
records := func(address string) map[string][]string {
|
|
return map[string][]string{
|
|
"A": {address},
|
|
"TXT": {"v=spf1 -all"},
|
|
}
|
|
}
|
|
|
|
before := hostnameState(map[string]map[string][]string{
|
|
nsA: records(ip1),
|
|
nsB: records(ip1),
|
|
})
|
|
after := hostnameState(map[string]map[string][]string{
|
|
nsA: records(ip1),
|
|
nsB: records(ip2),
|
|
})
|
|
|
|
// The hostname change detection uses only the notifier.
|
|
notifier := &mockNotifier{}
|
|
w := watcher.NewForTest(nil, nil, nil, nil, nil, notifier)
|
|
|
|
w.DetectHostnameChanges(t.Context(), host, before, after)
|
|
|
|
want := map[string]string{
|
|
"Record Change: " + host: `Hostname: www.example.net
|
|
Nameserver: b.ns.example.net.
|
|
Type: A
|
|
Old: 192.0.2.1
|
|
New: 192.0.2.2`,
|
|
"Inconsistency: " + host: `Hostname: www.example.net
|
|
Type: A
|
|
a.ns.example.net.: 192.0.2.1
|
|
b.ns.example.net.: 192.0.2.2`,
|
|
}
|
|
|
|
notifications := notifier.getNotifications()
|
|
if len(notifications) != len(want) {
|
|
t.Fatalf(
|
|
"sent %d notifications, want %d: %v",
|
|
len(notifications), len(want), notifications,
|
|
)
|
|
}
|
|
|
|
for _, n := range notifications {
|
|
if n.Message != want[n.Title] {
|
|
t.Errorf(
|
|
"%s message:\n%s\nwant:\n%s",
|
|
n.Title, n.Message, want[n.Title],
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Every kind of notification about a configured apex domain's own
|
|
// records names it as a domain.
|
|
func TestDomainRecordNotificationsNameTheDomain(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
notifier := &mockNotifier{}
|
|
w := watcher.NewForTest(
|
|
&config.Config{Domains: []string{domain}},
|
|
nil, nil, nil, nil, notifier,
|
|
)
|
|
|
|
// nsA's address changes, which also makes it differ from nsC; nsB
|
|
// fails; nsC answers again; nsD is gone.
|
|
nsD := "d.ns.example.net."
|
|
w.DetectHostnameChanges(t.Context(), domain,
|
|
saved(map[string]*state.NameserverRecordState{
|
|
nsA: answered(map[string][]string{"A": {ip1}}),
|
|
nsB: answered(map[string][]string{"A": {ip1}}),
|
|
nsC: failed(),
|
|
nsD: answered(map[string][]string{"A": {ip1}}),
|
|
}),
|
|
saved(map[string]*state.NameserverRecordState{
|
|
nsA: answered(map[string][]string{"A": {ip2}}),
|
|
nsB: failed(),
|
|
nsC: answered(map[string][]string{"A": {ip1}}),
|
|
}),
|
|
)
|
|
|
|
// The address at the end of its CNAME chain changes.
|
|
w.DetectHostnameChanges(
|
|
t.Context(), domain, cnameState(ip1), cnameState(ip2),
|
|
)
|
|
|
|
// NS Failure is sent for nsB failing and for nsD being gone.
|
|
want := map[string]int{
|
|
"Record Change": 1,
|
|
"Inconsistency": 1,
|
|
"NS Failure": 2,
|
|
"NS Recovery": 1,
|
|
"CNAME Address Change": 1,
|
|
}
|
|
|
|
sent := make(map[string]int)
|
|
|
|
for _, n := range notifier.getNotifications() {
|
|
kind, _, _ := strings.Cut(n.Title, ":")
|
|
sent[kind]++
|
|
|
|
if !strings.HasPrefix(n.Message, "Domain: "+domain+"\n") {
|
|
t.Errorf("%s message does not name the domain:\n%s",
|
|
n.Title, n.Message)
|
|
}
|
|
}
|
|
|
|
if !maps.Equal(sent, want) {
|
|
t.Errorf("sent %v, want %v", sent, want)
|
|
}
|
|
}
|
|
|
|
// The startup notification counts the configured domains and hostnames,
|
|
// although the state's hostnames also hold the apex domain's own
|
|
// records. Nothing is looked up: the watcher has no resolver.
|
|
func TestStartupNotificationCountsConfiguredNames(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := defaultTestConfig(t)
|
|
cfg.SendTestNotification = true
|
|
cfg.Domains = []string{domain}
|
|
cfg.Hostnames = []string{host}
|
|
|
|
deps := newTestDeps(t, cfg)
|
|
w := watcher.NewForTest(cfg, deps.state, nil, nil, nil, deps.notifier)
|
|
|
|
// The state a check of both names saves.
|
|
deps.state.SetDomainState(domain, &state.DomainState{
|
|
Nameservers: []string{nsA},
|
|
})
|
|
|
|
for _, name := range []string{domain, host} {
|
|
deps.state.SetHostnameState(name, saved(
|
|
map[string]*state.NameserverRecordState{
|
|
nsA: answered(map[string][]string{"A": {ip1}}),
|
|
},
|
|
))
|
|
}
|
|
|
|
w.MaybeSendTestNotification(t.Context())
|
|
|
|
notifications := deps.notifier.getNotifications()
|
|
|
|
counts := "\nMonitoring 1 domain(s) and 1 hostname(s).\n"
|
|
if len(notifications) != 1 ||
|
|
!strings.Contains(notifications[0].Message, counts) {
|
|
t.Errorf("sent %v, want one message with %q", notifications, counts)
|
|
}
|
|
}
|
|
|
|
// A Port Change notification lists the configured apex domain and the
|
|
// hostname that resolve to the port's address on separate lines. The
|
|
// port checks read the saved hostname state and look nothing up, so the
|
|
// watcher has no resolver.
|
|
func TestPortChangeListsDomainsApartFromHostnames(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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,
|
|
)
|
|
w.SetFirstRun(false)
|
|
|
|
// Both names resolve to ip1, whose port 443 the previous check
|
|
// found open. It is closed now.
|
|
for _, name := range []string{domain, host} {
|
|
deps.state.SetHostnameState(name, saved(
|
|
map[string]*state.NameserverRecordState{
|
|
nsA: answered(map[string][]string{"A": {ip1}}),
|
|
},
|
|
))
|
|
}
|
|
|
|
key := ip1 + ":443"
|
|
deps.state.SetPortState(key, &state.PortState{
|
|
Open: true, Hostnames: []string{domain, host},
|
|
})
|
|
deps.portChecker.closed = true
|
|
|
|
w.CheckAllPorts(t.Context())
|
|
|
|
title := "Port Change: " + key
|
|
want := `Domains: example.net
|
|
Hostnames: www.example.net
|
|
Address: 192.0.2.1:443
|
|
Port now closed`
|
|
|
|
got := deps.notifier.getNotifications()
|
|
if len(got) != 1 || got[0].Title != title || got[0].Message != want {
|
|
t.Errorf("sent %v, want one %q with message:\n%s", got, title, want)
|
|
}
|
|
}
|