check / check (push) Waiting to run
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
220 lines
5.6 KiB
Go
220 lines
5.6 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.
|
|
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"
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
|
|
now := time.Now()
|
|
|
|
st.SetHostnameState(testHostname, &state.HostnameState{
|
|
RecordsByNameserver: map[string]*state.NameserverRecordState{
|
|
answeringNS: {
|
|
Records: map[string][]string{"A": {"192.0.2.1"}},
|
|
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,
|
|
})
|
|
|
|
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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|