dashboard, status API and notifications tell apex domains from hostnames (closes #224)
check / check (push) Successful in 1m15s

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
This commit is contained in:
2026-10-02 07:56:23 +00:00
parent 1218df9467
commit 134cd9001a
11 changed files with 421 additions and 100 deletions
+62 -1
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"slices"
"testing"
"time"
@@ -19,7 +20,8 @@ import (
// The state the handler tests serve: www.example.com has one nameserver
// that answered and one whose query failed, and its certificate check
// failed.
// failed. example.net is an apex domain, whose own records are saved
// with the hostnames' records, as the watcher saves them.
const (
testHostname = "www.example.com"
answeringNS = "ns1.example.com."
@@ -27,6 +29,9 @@ const (
nsFailureReason = "server returned a referral"
certKey = "192.0.2.1:443:www.example.com"
certFailedReason = "x509: certificate has expired or is not yet valid"
testDomain = "example.net"
domainNS = "a.iana-servers.net."
domainAddress = "192.0.2.2"
)
// newHandlersWithFailures builds real Handlers whose state holds the
@@ -85,6 +90,22 @@ func newHandlersWithFailures(t *testing.T) *handlers.Handlers {
LastChecked: now,
})
st.SetDomainState(testDomain, &state.DomainState{
Nameservers: []string{domainNS},
LastChecked: now,
})
st.SetHostnameState(testDomain, &state.HostnameState{
RecordsByNameserver: map[string]*state.NameserverRecordState{
domainNS: {
Records: map[string][]string{"A": {domainAddress}},
Status: "ok",
LastChecked: now,
},
},
LastChecked: now,
})
hnd, err := handlers.New(nil, handlers.Params{
Logger: log,
Globals: glob,
@@ -156,3 +177,43 @@ func TestStatusGivesFailureReasons(t *testing.T) {
got, certFailedReason)
}
}
// TestStatusGivesDomainRecordsUnderTheDomain checks that /api/v1/status
// gives an apex domain's own records in its domain entry, and neither
// lists nor counts the domain as a hostname.
func TestStatusGivesDomainRecordsUnderTheDomain(t *testing.T) {
t.Parallel()
body := get(t, newHandlersWithFailures(t).HandleStatus())
var resp struct {
Counts struct {
Hostnames int `json:"hostnames"`
} `json:"counts"`
Domains map[string]struct {
RecordsByNameserver map[string]struct {
Records map[string][]string `json:"records"`
} `json:"recordsByNameserver"`
} `json:"domains"`
Hostnames map[string]any `json:"hostnames"`
}
err := json.Unmarshal([]byte(body), &resp)
if err != nil {
t.Fatalf("decoding response: %v", err)
}
if resp.Counts.Hostnames != 1 {
t.Errorf("counts.hostnames = %d, want 1", resp.Counts.Hostnames)
}
if _, listed := resp.Hostnames[testDomain]; listed {
t.Errorf("hostnames lists the domain %s", testDomain)
}
records := resp.Domains[testDomain].RecordsByNameserver[domainNS].Records
if !slices.Equal(records["A"], []string{domainAddress}) {
t.Errorf("domain %s records at %s = %v, want A %s",
testDomain, domainNS, records, domainAddress)
}
}