Files
dnswatcher/internal/handlers/dashboard_test.go
T
sneak a771f51b0b
check / check (push) Waiting to run
resolver, watcher: a domain's nameservers are only its own delegation (closes #222)
When a domain's parent zone's servers answer NXDOMAIN, LookupNS returns
ErrNXDomain. The watcher then saves the domain with no nameservers and
nxdomain set, shown on the dashboard and in /api/v1/status, asks for none
of its records and removes those saved, so its old nameservers go in one
NS Change. A domain with no delegation of its own gets an empty set and
its records are still asked at the zone it is in.
FindAuthoritativeNameservers moves to a parent name only on one of those
two answers; when the servers do not answer, it returns the error. After
an upgrade, a domain without its own delegation that was saved with its
parent zone's nameservers gets one NS Change; the README says so.

Model: opus-5-5
2026-10-02 10:24:40 +00:00

274 lines
7.3 KiB
Go

package handlers_test
import (
"regexp"
"strings"
"testing"
"time"
"sneak.berlin/go/dnswatcher/internal/handlers"
)
func TestRelTime(t *testing.T) {
t.Parallel()
tests := []struct {
name string
dur time.Duration
want string
}{
{"zero", 0, "never"},
{"seconds", 30 * time.Second, "30s ago"},
{"minutes", 5 * time.Minute, "5m ago"},
{"hours", 2*time.Hour + 15*time.Minute, "2h 15m ago"},
{"days", 48*time.Hour + 3*time.Hour, "2d 3h ago"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var input time.Time
if tt.dur > 0 {
input = time.Now().Add(-tt.dur)
}
got := handlers.RelTime(input)
if got != tt.want {
t.Errorf(
"RelTime(%v) = %q, want %q",
tt.dur, got, tt.want,
)
}
})
}
}
func TestExpiryDays(t *testing.T) {
t.Parallel()
// 10 days from now.
future := time.Now().Add(10 * 24 * time.Hour)
days := handlers.ExpiryDays(future)
if days < 9 || days > 10 {
t.Errorf("expected ~10 days, got %d", days)
}
// Already expired.
past := time.Now().Add(-24 * time.Hour)
days = handlers.ExpiryDays(past)
if days != 0 {
t.Errorf("expected 0 for expired, got %d", days)
}
}
func TestFormatRecords(t *testing.T) {
t.Parallel()
got := handlers.FormatRecords(nil)
if got != "-" {
t.Errorf("expected -, got %q", got)
}
got = handlers.FormatRecords(map[string][]string{
"A": {"1.2.3.4"},
})
if got != "A: 1.2.3.4" {
t.Errorf("unexpected format: %q", got)
}
}
// TestFormatRecordsTypeOrder checks that record types are listed in
// the README's order (A, AAAA, CNAME, MX, TXT, SRV, CAA, NS), with
// any other type after them in alphabetical order.
func TestFormatRecordsTypeOrder(t *testing.T) {
t.Parallel()
got := handlers.FormatRecords(map[string][]string{
"SOA": {"ns1.example.com. hostmaster.example.com. 1 2 3 4 5"},
"NS": {"ns1.example.com.", "ns2.example.com."},
"CAA": {`0 issue "letsencrypt.org"`},
"DNAME": {"example.net."},
"TXT": {"v=spf1 -all"},
"SRV": {"10 5 443 www.example.com."},
"MX": {"10 mail.example.com."},
"CNAME": {"www.example.com."},
"AAAA": {"2001:db8::1"},
"A": {"192.0.2.1"},
})
want := strings.Join([]string{
"A: 192.0.2.1",
"AAAA: 2001:db8::1",
"CNAME: www.example.com.",
"MX: 10 mail.example.com.",
"TXT: v=spf1 -all",
"SRV: 10 5 443 www.example.com.",
`CAA: 0 issue "letsencrypt.org"`,
"NS: ns1.example.com.",
"NS: ns2.example.com.",
"DNAME: example.net.",
"SOA: ns1.example.com. hostmaster.example.com. 1 2 3 4 5",
}, ", ")
if got != want {
t.Errorf("FormatRecords lists types out of order:\n got %q\nwant %q",
got, want)
}
}
// dashboardRow returns the table row of page that contains name.
func dashboardRow(t *testing.T, page string, name string) string {
t.Helper()
for row := range strings.SplitSeq(page, "<tr") {
if strings.Contains(row, name) {
return row
}
}
t.Fatalf("dashboard has no row containing %q", name)
return ""
}
// TestDashboardShowsFailureReasons checks that the dashboard shows the
// reason in the row of a failed nameserver and of a failed certificate,
// and not in the row of a nameserver that answered.
func TestDashboardShowsFailureReasons(t *testing.T) {
t.Parallel()
page := get(t, newHandlersWithFailures(t).HandleDashboard())
if !strings.Contains(dashboardRow(t, page, failedNS), nsFailureReason) {
t.Errorf("row of %s does not show %q", failedNS, nsFailureReason)
}
if strings.Contains(dashboardRow(t, page, answeringNS), nsFailureReason) {
t.Errorf("row of %s shows %q", answeringNS, nsFailureReason)
}
if !strings.Contains(dashboardRow(t, page, certKey), certFailedReason) {
t.Errorf("row of %s does not show %q", certKey, certFailedReason)
}
}
// dashboardSection returns the section of page under heading.
func dashboardSection(t *testing.T, page string, heading string) string {
t.Helper()
for section := range strings.SplitSeq(page, "<section") {
words := strings.Join(strings.Fields(section), " ")
if strings.Contains(words, "> "+heading+" </h2>") {
return section
}
}
t.Fatalf("dashboard has no section headed %q", heading)
return ""
}
// TestDashboardShowsDomainRecordsUnderDomains checks that the dashboard
// shows an apex domain's own records in the Domains section, and
// neither lists nor counts the domain as a hostname.
func TestDashboardShowsDomainRecordsUnderDomains(t *testing.T) {
t.Parallel()
page := get(t, newHandlersWithFailures(t).HandleDashboard())
domains := dashboardSection(t, page, "Domains")
if !strings.Contains(dashboardRow(t, domains, domainAddress), testDomain) {
t.Errorf("row of %s does not name %s", domainAddress, testDomain)
}
if strings.Contains(dashboardSection(t, page, "Hostnames"), testDomain) {
t.Errorf("Hostnames section lists the domain %s", testDomain)
}
words := strings.Join(strings.Fields(page), " ")
footer := "monitoring 2 domains + 1 hostnames"
if !strings.Contains(words, footer) {
t.Errorf("dashboard does not say %q", footer)
}
// With the tags taken out, the summary bar starts "Domains 2
// Hostnames 1".
text := regexp.MustCompile(`<[^>]*>`).ReplaceAllString(page, " ")
summary := "Domains 2 Hostnames 1"
if !strings.Contains(strings.Join(strings.Fields(text), " "), summary) {
t.Errorf("summary bar does not say %q", summary)
}
}
// TestDashboardMarksDomainThatDoesNotExist checks that the Domains
// section says a domain that does not exist does not exist, and does
// not say so of a domain that exists.
func TestDashboardMarksDomainThatDoesNotExist(t *testing.T) {
t.Parallel()
page := get(t, newHandlersWithFailures(t).HandleDashboard())
domains := dashboardSection(t, page, "Domains")
if !strings.Contains(dashboardRow(t, domains, missingDomain), "does not exist") {
t.Errorf("row of %s does not say it does not exist", missingDomain)
}
if strings.Contains(dashboardRow(t, domains, testDomain), "does not exist") {
t.Errorf("row of %s says it does not exist", testDomain)
}
}
// rowCells returns the text of each cell of a dashboard table row
// whose cells start with tag, "<th" or "<td".
func rowCells(row string, tag string) []string {
tags := regexp.MustCompile(`<[^>]*>`)
parts := strings.Split(row, tag)[1:]
cells := make([]string, 0, len(parts))
for _, cell := range parts {
text := tags.ReplaceAllString(tag+cell, " ")
cells = append(cells, strings.Join(strings.Fields(text), " "))
}
return cells
}
// TestDashboardPortsTellDomainsFromHostnames checks that the Ports
// table lists an apex domain under Domains and a hostname under
// Hostnames when both resolve to the port's address.
func TestDashboardPortsTellDomainsFromHostnames(t *testing.T) {
t.Parallel()
page := get(t, newHandlersWithFailures(t).HandleDashboard())
ports := dashboardSection(t, page, "Ports")
headings := rowCells(dashboardRow(t, ports, "Address</th>"), "<th")
cells := rowCells(dashboardRow(t, ports, sharedPort), "<td")
if len(cells) != len(headings) {
t.Fatalf("row of %s has cells %q under headings %q",
sharedPort, cells, headings)
}
under := make(map[string]string)
for i, heading := range headings {
under[heading] = cells[i]
}
if under["Domains"] != testDomain {
t.Errorf("row of %s lists %q under Domains, want %q",
sharedPort, under["Domains"], testDomain)
}
if under["Hostnames"] != testHostname {
t.Errorf("row of %s lists %q under Hostnames, want %q",
sharedPort, under["Hostnames"], testHostname)
}
}