check / check (push) Waiting to run
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
268 lines
6.9 KiB
Go
268 lines
6.9 KiB
Go
package handlers_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"go.uber.org/fx/fxtest"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/config"
|
|
"sneak.berlin/go/dnswatcher/internal/globals"
|
|
"sneak.berlin/go/dnswatcher/internal/handlers"
|
|
"sneak.berlin/go/dnswatcher/internal/logger"
|
|
"sneak.berlin/go/dnswatcher/internal/notify"
|
|
"sneak.berlin/go/dnswatcher/internal/state"
|
|
)
|
|
|
|
// The state the handler tests serve: www.example.com has one nameserver
|
|
// that answered and one whose query failed, and its certificate check
|
|
// failed. example.net is an apex domain, whose own records are saved
|
|
// with the hostnames' records, as the watcher saves them. Both names
|
|
// resolve to domainAddress, whose port 443 entry lists them.
|
|
const (
|
|
testHostname = "www.example.com"
|
|
answeringNS = "ns1.example.com."
|
|
failedNS = "ns2.example.com."
|
|
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"
|
|
sharedPort = domainAddress + ":443"
|
|
)
|
|
|
|
// newHandlersWithFailures builds real Handlers whose state holds the
|
|
// entries described above.
|
|
func newHandlersWithFailures(t *testing.T) *handlers.Handlers {
|
|
t.Helper()
|
|
|
|
glob, err := globals.New(nil)
|
|
if err != nil {
|
|
t.Fatalf("globals.New: %v", err)
|
|
}
|
|
|
|
log, err := logger.New(nil, logger.Params{Globals: glob})
|
|
if err != nil {
|
|
t.Fatalf("logger.New: %v", err)
|
|
}
|
|
|
|
notifier, err := notify.New(fxtest.NewLifecycle(t), notify.Params{
|
|
Logger: log,
|
|
Config: &config.Config{},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("notify.New: %v", err)
|
|
}
|
|
|
|
st, err := state.New(fxtest.NewLifecycle(t), state.Params{
|
|
Logger: log,
|
|
Config: &config.Config{DataDir: t.TempDir()},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("state.New: %v", err)
|
|
}
|
|
|
|
setTestState(st)
|
|
|
|
hnd, err := handlers.New(nil, handlers.Params{
|
|
Logger: log,
|
|
Globals: glob,
|
|
State: st,
|
|
Notify: notifier,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("handlers.New: %v", err)
|
|
}
|
|
|
|
return hnd
|
|
}
|
|
|
|
// setTestState sets the entries described above in st.
|
|
func setTestState(st *state.State) {
|
|
now := time.Now()
|
|
|
|
st.SetHostnameState(testHostname, &state.HostnameState{
|
|
RecordsByNameserver: map[string]*state.NameserverRecordState{
|
|
answeringNS: {
|
|
Records: map[string][]string{
|
|
"A": {"192.0.2.1", domainAddress},
|
|
},
|
|
Status: "ok",
|
|
LastChecked: now,
|
|
},
|
|
failedNS: {
|
|
Records: map[string][]string{},
|
|
Status: "error",
|
|
Error: nsFailureReason,
|
|
LastChecked: now,
|
|
},
|
|
},
|
|
LastChecked: now,
|
|
})
|
|
|
|
st.SetCertificateState(certKey, &state.CertificateState{
|
|
Status: "error",
|
|
Error: certFailedReason,
|
|
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,
|
|
})
|
|
|
|
st.SetPortState(sharedPort, &state.PortState{
|
|
Open: true,
|
|
Hostnames: []string{testDomain, testHostname},
|
|
LastChecked: now,
|
|
})
|
|
}
|
|
|
|
// get serves one GET request to handler and returns the response body.
|
|
func get(t *testing.T, handler http.HandlerFunc) string {
|
|
t.Helper()
|
|
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequestWithContext(
|
|
t.Context(), http.MethodGet, "/", nil,
|
|
)
|
|
|
|
handler(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200", rec.Code)
|
|
}
|
|
|
|
return rec.Body.String()
|
|
}
|
|
|
|
// TestStatusGivesFailureReasons checks that /api/v1/status gives the
|
|
// reason for a failed nameserver entry and a failed certificate entry,
|
|
// and no error for a nameserver that answered.
|
|
func TestStatusGivesFailureReasons(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
body := get(t, newHandlersWithFailures(t).HandleStatus())
|
|
|
|
var resp struct {
|
|
Hostnames map[string]struct {
|
|
Nameservers map[string]map[string]any `json:"nameservers"`
|
|
} `json:"hostnames"`
|
|
Certificates map[string]map[string]any `json:"certificates"`
|
|
}
|
|
|
|
err := json.Unmarshal([]byte(body), &resp)
|
|
if err != nil {
|
|
t.Fatalf("decoding response: %v", err)
|
|
}
|
|
|
|
nameservers := resp.Hostnames[testHostname].Nameservers
|
|
|
|
got := nameservers[failedNS]["error"]
|
|
if got != nsFailureReason {
|
|
t.Errorf("failed nameserver error = %v, want %q",
|
|
got, nsFailureReason)
|
|
}
|
|
|
|
_, has := nameservers[answeringNS]["error"]
|
|
if has {
|
|
t.Errorf("answering nameserver has an error field: %v",
|
|
nameservers[answeringNS])
|
|
}
|
|
|
|
got = resp.Certificates[certKey]["error"]
|
|
if got != certFailedReason {
|
|
t.Errorf("failed certificate error = %v, want %q",
|
|
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)
|
|
}
|
|
}
|
|
|
|
// TestStatusPortsTellDomainsFromHostnames checks that a port entry in
|
|
// /api/v1/status lists an apex domain in domains and a hostname in
|
|
// hostnames when both resolve to its address.
|
|
func TestStatusPortsTellDomainsFromHostnames(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
body := get(t, newHandlersWithFailures(t).HandleStatus())
|
|
|
|
var resp struct {
|
|
Ports map[string]struct {
|
|
Domains []string `json:"domains"`
|
|
Hostnames []string `json:"hostnames"`
|
|
} `json:"ports"`
|
|
}
|
|
|
|
err := json.Unmarshal([]byte(body), &resp)
|
|
if err != nil {
|
|
t.Fatalf("decoding response: %v", err)
|
|
}
|
|
|
|
port := resp.Ports[sharedPort]
|
|
|
|
if !slices.Equal(port.Domains, []string{testDomain}) {
|
|
t.Errorf("port %s domains = %v, want [%s]",
|
|
sharedPort, port.Domains, testDomain)
|
|
}
|
|
|
|
if !slices.Equal(port.Hostnames, []string{testHostname}) {
|
|
t.Errorf("port %s hostnames = %v, want [%s]",
|
|
sharedPort, port.Hostnames, testHostname)
|
|
}
|
|
}
|