check / check (push) Successful in 1m15s
An apex domain's own records are still saved with the hostnames' records, under the domain's name, so the port and TLS checks find its addresses. Notifications about them now start `Domain:`, decided by the configured domains. The dashboard and /api/v1/status, which read only the saved state, take a hostname entry whose name also has a domain entry as that domain's own records: the dashboard shows them in a second table under Domains, the API in the domain's `recordsByNameserver`, and neither lists or counts them as hostnames. The startup notification counts domains and hostnames from the configuration. README says which of a domain's own records are watched and how their changes are notified. Model: opus-5-5
168 lines
4.4 KiB
Go
168 lines
4.4 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)
|
|
}
|
|
}
|