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) } }