check / check (push) Canceled after 0s
For a name with a CNAME, a nameserver answers a query of any type with that CNAME, and the records of every answer were added, so the CNAME was stored once for each of the eight record types asked for. collectAnswerRecords now adds each value once per record type. A state file saved before this holds the repeated values. Load keeps each record value once, so the first check after upgrading sees no record change and notifies nothing for them. Model: opus-5-5
236 lines
6.1 KiB
Go
236 lines
6.1 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 the first check and disagreeing alerts once",
|
|
loaded: onlyA,
|
|
checks: []map[string]map[string][]string{disagree, disagree},
|
|
want: [][][2]string{alert, 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, records := range tt.checks {
|
|
current := hostnameState(records)
|
|
|
|
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 = current
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInconsistencyAlert(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}}}
|
|
|
|
// 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,
|
|
},
|
|
{
|
|
name: "nameserver new on the first check and disagreeing alerts once",
|
|
loaded: onlyA,
|
|
want: 1,
|
|
},
|
|
}
|
|
|
|
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 {
|
|
current := hostnameState(disagree)
|
|
w.DetectHostnameChanges(t.Context(), host, prev, current)
|
|
prev = current
|
|
}
|
|
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestFirstCheckAfterRepeatedValuesLoaded saves a state file holding a
|
|
// hostname's CNAME once for every record type asked for, as checks did
|
|
// before each value was stored once, and loads it. A check that then
|
|
// finds the CNAME once at each nameserver must notify nothing.
|
|
func TestFirstCheckAfterRepeatedValuesLoaded(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const cname = "c.example.net."
|
|
|
|
cfg := defaultTestConfig(t)
|
|
repeated := map[string][]string{
|
|
"CNAME": {cname, cname, cname, cname, cname, cname, cname, cname},
|
|
}
|
|
once := map[string][]string{"CNAME": {cname}}
|
|
|
|
saved := newTestDeps(t, cfg).state
|
|
saved.SetHostnameState(host, hostnameState(map[string]map[string][]string{
|
|
nsA: repeated, nsB: repeated,
|
|
}))
|
|
|
|
err := saved.Save()
|
|
if err != nil {
|
|
t.Fatalf("saving the state file: %v", err)
|
|
}
|
|
|
|
deps := newTestDeps(t, cfg)
|
|
|
|
err = deps.state.Load()
|
|
if err != nil {
|
|
t.Fatalf("loading the state file: %v", err)
|
|
}
|
|
|
|
prev, ok := deps.state.GetHostnameState(host)
|
|
if !ok {
|
|
t.Fatal("the state file has no state for " + host)
|
|
}
|
|
|
|
current := hostnameState(map[string]map[string][]string{
|
|
nsA: once, nsB: once,
|
|
})
|
|
|
|
// The hostname change detection uses only the notifier.
|
|
w := watcher.NewForTest(nil, nil, nil, nil, nil, deps.notifier)
|
|
w.DetectHostnameChanges(t.Context(), host, prev, current)
|
|
|
|
if got := deps.notifier.getNotifications(); len(got) != 0 {
|
|
t.Errorf("sent %v, want no notification", got)
|
|
}
|
|
}
|