check / check (push) Waiting to run
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
274 lines
7.3 KiB
Go
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)
|
|
}
|
|
}
|