check / check (push) Canceled after 0s
A port entry in the state saves the apex domains that resolve to its address with its hostnames. The dashboard's Ports table now has a Domains column next to Hostnames, and a port entry in /api/v1/status has a `domains` list, with `hostnames` no longer holding a domain. A name is taken as a domain when it has a domain entry, as the dashboard and API already tell a domain's own records from a hostname's. Both read the split from one function, buildPorts. The state file is unchanged; README says its port `hostnames` include domains. Model: opus-5-5
256 lines
6.7 KiB
Go
256 lines
6.7 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)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|