watcher: change messages name only the record types that differ (closes #219) #238

Merged
clawbot merged 1 commits from issue-219-change-messages-name-the-type into next 2026-10-02 08:46:57 +02:00
3 changed files with 126 additions and 6 deletions
+2
View File
@@ -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
+66
View File
@@ -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],
)
}
}
}
+56 -4
View File
@@ -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,
recordDifferences(
ns1, current.RecordsByNameserver[ns1].Records, ns1, current.RecordsByNameserver[ns1].Records,
ns2, current.RecordsByNameserver[ns2].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