check / check (push) Canceled after 0s
An apex domain's own records are still saved with the hostnames' records, under the domain's name, so the port and TLS checks find its addresses. Notifications about them now start `Domain:`, decided by the configured domains. The dashboard and /api/v1/status, which read only the saved state, take a hostname entry whose name also has a domain entry as that domain's own records: the dashboard shows them in a second table under Domains, the API in the domain's `recordsByNameserver`, and neither lists or counts them as hostnames. The startup notification counts domains and hostnames from the configuration. README says which of a domain's own records are watched and how their changes are notified. Model: opus-5-5
208 lines
5.2 KiB
Go
208 lines
5.2 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 1 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 1
|
|
// Hostnames 1".
|
|
text := regexp.MustCompile(`<[^>]*>`).ReplaceAllString(page, " ")
|
|
summary := "Domains 1 Hostnames 1"
|
|
|
|
if !strings.Contains(strings.Join(strings.Fields(text), " "), summary) {
|
|
t.Errorf("summary bar does not say %q", summary)
|
|
}
|
|
}
|