watcher: change messages name only the record types that differ (closes #219) #238
@@ -19,6 +19,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
|
- 2026-10-02: Record Change and Inconsistency notifications list only the record
|
||||||
|
types that differ, each with its values as plain text (closes #219).
|
||||||
- 2026-10-02: the startup notification no longer says every notification
|
- 2026-10-02: the startup notification no longer says every notification
|
||||||
endpoint works; it says it is a test sent to each of them (closes #230).
|
endpoint works; it says it is a test sent to each of them (closes #230).
|
||||||
- 2026-10-02: a Mattermost webhook that answers an HTTP error is logged as
|
- 2026-10-02: a Mattermost webhook that answers an HTTP error is logged as
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package watcher_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -570,10 +570,12 @@ func (w *Watcher) detectRecordChanges(
|
|||||||
}
|
}
|
||||||
|
|
||||||
msg := fmt.Sprintf(
|
msg := fmt.Sprintf(
|
||||||
"Hostname: %s\nNameserver: %s\n"+
|
"Hostname: %s\nNameserver: %s\n%s",
|
||||||
"Old: %v\nNew: %v",
|
|
||||||
hostname, ns,
|
hostname, ns,
|
||||||
prevNS.Records, cur.Records,
|
recordDifferences(
|
||||||
|
"Old", prevNS.Records,
|
||||||
|
"New", cur.Records,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
w.notify.SendNotification(
|
w.notify.SendNotification(
|
||||||
@@ -661,10 +663,12 @@ func (w *Watcher) detectInconsistencies(
|
|||||||
ns1, ns2 := pair[0], pair[1]
|
ns1, ns2 := pair[0], pair[1]
|
||||||
|
|
||||||
msg := fmt.Sprintf(
|
msg := fmt.Sprintf(
|
||||||
"Hostname: %s\n%s: %v\n%s: %v",
|
"Hostname: %s\n%s",
|
||||||
hostname,
|
hostname,
|
||||||
ns1, current.RecordsByNameserver[ns1].Records,
|
recordDifferences(
|
||||||
ns2, current.RecordsByNameserver[ns2].Records,
|
ns1, current.RecordsByNameserver[ns1].Records,
|
||||||
|
ns2, current.RecordsByNameserver[ns2].Records,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
w.notify.SendNotification(
|
w.notify.SendNotification(
|
||||||
@@ -1214,6 +1218,54 @@ func recordsEqual(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recordDifferences describes, in sorted order of type, each record
|
||||||
|
// type whose values differ between a and b: a line naming the type,
|
||||||
|
// then a line with a's values after labelA and one with b's after
|
||||||
|
// labelB. Types with the same values in both are left out.
|
||||||
|
func recordDifferences(
|
||||||
|
labelA string, a map[string][]string,
|
||||||
|
labelB string, b map[string][]string,
|
||||||
|
) string {
|
||||||
|
types := make([]string, 0, len(a)+len(b))
|
||||||
|
|
||||||
|
for recordType := range a {
|
||||||
|
types = append(types, recordType)
|
||||||
|
}
|
||||||
|
|
||||||
|
for recordType := range b {
|
||||||
|
if _, ok := a[recordType]; !ok {
|
||||||
|
types = append(types, recordType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(types)
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
for _, recordType := range types {
|
||||||
|
if sliceEqual(a[recordType], b[recordType]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = append(lines,
|
||||||
|
"Type: "+recordType,
|
||||||
|
labelA+": "+joinValues(a[recordType]),
|
||||||
|
labelB+": "+joinValues(b[recordType]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// joinValues lists record values separated by commas, or says none.
|
||||||
|
func joinValues(values []string) string {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(values, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
func sliceEqual(a, b []string) bool {
|
func sliceEqual(a, b []string) bool {
|
||||||
if len(a) != len(b) {
|
if len(a) != len(b) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
Reference in New Issue
Block a user