package handler import ( "context" "encoding/json" "log/slog" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "codeberg.org/VARASYS/ZDDC/zddc/internal/config" ) // adminTestRoot creates a temp dir, writes a .zddc with the given admins // list, and returns a Config pointing at it. func adminTestRoot(t *testing.T, admins []string) (config.Config, *LogRing) { t.Helper() root := t.TempDir() if len(admins) > 0 { var b strings.Builder b.WriteString("admins:\n") for _, a := range admins { b.WriteString(" - \"") b.WriteString(a) b.WriteString("\"\n") } if err := os.WriteFile(filepath.Join(root, ".zddc"), []byte(b.String()), 0o644); err != nil { t.Fatalf("write .zddc: %v", err) } } return config.Config{ Root: root, Addr: ":8443", EmailHeader: "X-Auth-Request-Email", }, NewLogRing(50) } // requestWithEmail builds a request whose context already carries email (as // the real ACLMiddleware would inject) and whose path is path. func requestWithEmail(method, path, email string) *http.Request { r := httptest.NewRequest(method, path, nil) if email != "" { r.Header.Set("X-Auth-Request-Email", email) ctx := context.WithValue(r.Context(), EmailKey, email) r = r.WithContext(ctx) } return r } func TestServeAdminAuthGate(t *testing.T) { cfg, ring := adminTestRoot(t, []string{"alice@example.com"}) cases := []struct { name string path string email string wantStatus int }{ // Anonymous (no email) — every path is hidden. {"anonymous /.admin/", "/.admin/", "", http.StatusNotFound}, {"anonymous /.admin/whoami", "/.admin/whoami", "", http.StatusNotFound}, {"anonymous /.admin/config", "/.admin/config", "", http.StatusNotFound}, {"anonymous /.admin/logs", "/.admin/logs", "", http.StatusNotFound}, // Logged-in non-admin — 404 (existence not leaked). {"non-admin /.admin/", "/.admin/", "bob@example.com", http.StatusNotFound}, {"non-admin /.admin/whoami", "/.admin/whoami", "bob@example.com", http.StatusNotFound}, // Admin — every defined path responds 200. {"admin /.admin/", "/.admin/", "alice@example.com", http.StatusOK}, {"admin /.admin/whoami", "/.admin/whoami", "alice@example.com", http.StatusOK}, {"admin /.admin/config", "/.admin/config", "alice@example.com", http.StatusOK}, {"admin /.admin/logs", "/.admin/logs", "alice@example.com", http.StatusOK}, // Admin hitting an undefined sub-route — 404. {"admin unknown subroute", "/.admin/nope", "alice@example.com", http.StatusNotFound}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { rec := httptest.NewRecorder() ServeAdmin(cfg, ring, rec, requestWithEmail(http.MethodGet, tc.path, tc.email)) if rec.Code != tc.wantStatus { t.Errorf("status = %d, want %d (body: %s)", rec.Code, tc.wantStatus, rec.Body.String()) } }) } } func TestServeAdminWhoamiPayload(t *testing.T) { cfg, ring := adminTestRoot(t, []string{"alice@example.com"}) rec := httptest.NewRecorder() r := requestWithEmail(http.MethodGet, "/.admin/whoami", "alice@example.com") r.Header.Set("X-Other-Header", "hi there") ServeAdmin(cfg, ring, rec, r) if rec.Code != 200 { t.Fatalf("status = %d", rec.Code) } if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "application/json") { t.Errorf("Content-Type = %q, want application/json", ct) } var got map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { t.Fatalf("invalid JSON: %v\n%s", err, rec.Body.String()) } if got["configured_email_header"] != "X-Auth-Request-Email" { t.Errorf("configured_email_header = %v", got["configured_email_header"]) } if got["observed_email"] != "alice@example.com" { t.Errorf("observed_email = %v", got["observed_email"]) } if got["resolved_email"] != "alice@example.com" { t.Errorf("resolved_email = %v", got["resolved_email"]) } headers, _ := got["headers"].(map[string]any) if _, ok := headers["X-Auth-Request-Email"]; !ok { t.Errorf("headers map missing X-Auth-Request-Email: %+v", headers) } if _, ok := headers["X-Other-Header"]; !ok { t.Errorf("headers map missing X-Other-Header: %+v", headers) } } func TestServeAdminConfigPayload(t *testing.T) { cfg, ring := adminTestRoot(t, []string{"alice@example.com"}) cfg.LogLevel = "info" cfg.IndexPath = ".archive" cfg.CORSOrigins = []string{"https://zddc.varasys.io"} rec := httptest.NewRecorder() ServeAdmin(cfg, ring, rec, requestWithEmail(http.MethodGet, "/.admin/config", "alice@example.com")) if rec.Code != 200 { t.Fatalf("status = %d", rec.Code) } var got map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { t.Fatalf("invalid JSON: %v", err) } for _, want := range []string{"root", "addr", "email_header", "log_level", "cors_origins"} { if _, ok := got[want]; !ok { t.Errorf("config payload missing key %q: %+v", want, got) } } if got["email_header"] != "X-Auth-Request-Email" { t.Errorf("email_header = %v", got["email_header"]) } } func TestServeAdminLogsPayload(t *testing.T) { cfg, ring := adminTestRoot(t, []string{"alice@example.com"}) rh := NewRingHandler(ring, slog.LevelDebug) logger := slog.New(rh) logger.Info("first") logger.Warn("second", "code", 42) rec := httptest.NewRecorder() ServeAdmin(cfg, ring, rec, requestWithEmail(http.MethodGet, "/.admin/logs", "alice@example.com")) if rec.Code != 200 { t.Fatalf("status = %d", rec.Code) } var got []map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil { t.Fatalf("invalid JSON: %v\n%s", err, rec.Body.String()) } if len(got) != 2 { t.Fatalf("entries = %d, want 2", len(got)) } if got[0]["message"] != "first" || got[1]["message"] != "second" { t.Errorf("ordering wrong: %v / %v", got[0]["message"], got[1]["message"]) } } func TestServeAdminLogsLevelFilter(t *testing.T) { cfg, ring := adminTestRoot(t, []string{"alice@example.com"}) rh := NewRingHandler(ring, slog.LevelDebug) logger := slog.New(rh) logger.Debug("d") logger.Info("i") logger.Warn("w") rec := httptest.NewRecorder() ServeAdmin(cfg, ring, rec, requestWithEmail(http.MethodGet, "/.admin/logs?level=warn", "alice@example.com")) var got []map[string]any _ = json.Unmarshal(rec.Body.Bytes(), &got) if len(got) != 1 || got[0]["message"] != "w" { t.Errorf("level=warn filter failed: %+v", got) } } func TestServeAdminDashboardHTML(t *testing.T) { cfg, ring := adminTestRoot(t, []string{"alice@example.com"}) rec := httptest.NewRecorder() ServeAdmin(cfg, ring, rec, requestWithEmail(http.MethodGet, "/.admin/", "alice@example.com")) if rec.Code != 200 { t.Fatalf("status = %d", rec.Code) } if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/html") { t.Errorf("Content-Type = %q, want text/html", ct) } body := rec.Body.String() for _, want := range []string{"", "/.admin/", `data-target="whoami"`, `data-target="config"`, `data-target="logs"`} { if !strings.Contains(body, want) { t.Errorf("dashboard missing %q", want) } } } func TestServeAdminNoAdminsConfiguredHidesEverything(t *testing.T) { // .zddc exists but has no admins list — page is invisible to all. cfg, ring := adminTestRoot(t, nil) if err := os.WriteFile(filepath.Join(cfg.Root, ".zddc"), []byte("acl:\n allow: [\"*\"]\n"), 0o644); err != nil { t.Fatalf("write .zddc: %v", err) } rec := httptest.NewRecorder() ServeAdmin(cfg, ring, rec, requestWithEmail(http.MethodGet, "/.admin/whoami", "alice@example.com")) if rec.Code != http.StatusNotFound { t.Errorf("status = %d, want 404 (no admins configured)", rec.Code) } }