package handlers_test import ( "encoding/json" "net/http" "net/http/httptest" "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. 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" ) // 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, }) 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) } }