package watcher_test import ( "slices" "testing" "sneak.berlin/go/dnswatcher/internal/state" "sneak.berlin/go/dnswatcher/internal/watcher" ) const ( host = "www.example.net" nsA = "a.ns.example.net." nsB = "b.ns.example.net." nsC = "c.ns.example.net." ip1 = "192.0.2.1" ip2 = "192.0.2.2" ip3 = "192.0.2.3" ) // hostnameState builds the state a check with these records leaves behind. func hostnameState( records map[string]map[string][]string, ) *state.HostnameState { hs := &state.HostnameState{ RecordsByNameserver: make(map[string]*state.NameserverRecordState), } for ns, recs := range records { hs.RecordsByNameserver[ns] = &state.NameserverRecordState{ Records: recs, Status: "ok", } } return hs } func TestNewlyDisagreeingPairs(t *testing.T) { t.Parallel() onlyA := map[string]map[string][]string{nsA: {"A": {ip1}}} agree := map[string]map[string][]string{nsA: {"A": {ip1}}, nsB: {"A": {ip1}}} disagree := map[string]map[string][]string{nsA: {"A": {ip1}}, nsB: {"A": {ip2}}} alert := [][2]string{{nsA, nsB}} // b already disagrees with a and c; then c changes, so a and c, // which agreed, now differ. bDiffers := map[string]map[string][]string{ nsA: {"A": {ip1}}, nsB: {"A": {ip2}}, nsC: {"A": {ip1}}, } cChanges := map[string]map[string][]string{ nsA: {"A": {ip1}}, nsB: {"A": {ip2}}, nsC: {"A": {ip3}}, } // Each case starts from the state loaded at startup and runs the // checks in order; want[i] is what check i alerts for. tests := []struct { name string loaded map[string]map[string][]string checks []map[string]map[string][]string want [][][2]string }{ { name: "disagreement persisting across checks alerts once", loaded: agree, checks: []map[string]map[string][]string{disagree, disagree, disagree}, want: [][][2]string{alert, nil, nil}, }, { name: "disagreement starting on a later check alerts on it", loaded: agree, checks: []map[string]map[string][]string{agree, agree, disagree}, want: [][][2]string{nil, nil, alert}, }, { name: "disagreement in the loaded state does not alert", loaded: disagree, checks: []map[string]map[string][]string{disagree, disagree}, want: [][][2]string{nil, nil}, }, { name: "nameserver new on this check does not alert", loaded: onlyA, checks: []map[string]map[string][]string{disagree}, want: [][][2]string{nil}, }, { name: "disagreement after agreeing again alerts again", loaded: agree, checks: []map[string]map[string][]string{disagree, agree, disagree}, want: [][][2]string{alert, nil, alert}, }, { name: "new disagreement while another nameserver differs alerts", loaded: bDiffers, checks: []map[string]map[string][]string{cChanges, cChanges}, want: [][][2]string{{{nsA, nsC}}, nil}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() prev := hostnameState(tt.loaded) for i, current := range tt.checks { got := watcher.NewlyDisagreeingPairs(prev, current) if !slices.Equal(got, tt.want[i]) { t.Errorf( "check %d: alerted for %v, want %v", i, got, tt.want[i], ) } prev = hostnameState(current) } }) } } func TestInconsistencyAlert(t *testing.T) { t.Parallel() agree := map[string]map[string][]string{nsA: {"A": {ip1}}, nsB: {"A": {ip1}}} disagree := map[string]map[string][]string{nsA: {"A": {ip1}}, nsB: {"A": {ip2}}} // Each case starts from the state loaded at startup and then sees // the nameservers disagree on three checks in a row. tests := []struct { name string loaded map[string]map[string][]string want int }{ { name: "disagreement lasting several checks alerts once", loaded: agree, want: 1, }, { name: "disagreement in the loaded state does not alert", loaded: disagree, want: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() // The hostname change detection uses only the notifier. notifier := &mockNotifier{} w := watcher.NewForTest(nil, nil, nil, nil, nil, notifier) prev := hostnameState(tt.loaded) for range 3 { w.DetectHostnameChanges(t.Context(), host, prev, disagree) prev = hostnameState(disagree) } got := 0 for _, n := range notifier.getNotifications() { if n.Title == "Inconsistency: "+host { got++ } } if got != tt.want { t.Errorf("sent %d inconsistency alerts, want %d", got, tt.want) } }) } }