check / check (push) Successful in 1m19s
detectInconsistencies alerted for neighbouring pairs of nameservers whose records differed, on every DNS check, for as long as they differed. It now also takes the previous hostname state, compares every pair of nameservers, and alerts for a pair only when both were in that state with equal records. The state loaded at startup is the previous state for the first check, so a disagreement saved before a restart is not reported again. The choice of pairs is tested on record data, and the alert itself through the hostname change detection with the notifier stand-in and no resolver. The README describes the new behaviour. Model: opus-5-5
177 lines
4.4 KiB
Go
177 lines
4.4 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|