package httpapi import ( "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "webui4frpc/internal/store" ) // newAuditHarness builds a Handler + mux with a temp store, mirroring // TestSaveCanvasPublishesRevokeTask's setup minus the ring. func newAuditHarness(t *testing.T) (*Handler, http.Handler) { t.Helper() dir := t.TempDir() st, err := store.New(filepath.Join(dir, "test.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { st.Close() }) if _, err := st.CreateUser("auditor", "pw-auditor", "viewer"); err != nil { t.Fatal(err) } if _, _, err := st.CreateApiKey(1, "ci-key", "read"); err != nil { t.Fatal(err) } h := &Handler{Store: st, WorkDir: dir, User: "admin", Password: "pw"} mux, err := NewServeMux(h) if err != nil { t.Fatal(err) } return h, mux } func TestCsvEscape(t *testing.T) { cases := []struct{ in, want string }{ {"plain", "plain"}, {"", ""}, {"a,b", `"a,b"`}, {`say "hi"`, `"say ""hi"""`}, {"line\nbreak", "\"line\nbreak\""}, {"=cmd()", "'=cmd()"}, // formula injection defused {"+1+1", "'+1+1"}, // formula injection defused {"@SUM(A1)", "'@SUM(A1)"}, // formula injection defused {"-2+3", "'-2+3"}, // formula injection defused } for _, c := range cases { if got := csvEscape(c.in); got != c.want { t.Errorf("csvEscape(%q)=%q want %q", c.in, got, c.want) } } } func TestIsoTimeEmpty(t *testing.T) { if got := isoTime(0); got != "" { t.Errorf("isoTime(0)=%q want empty", got) } } func TestAuditUsersCsv(t *testing.T) { _, mux := newAuditHarness(t) req := httptest.NewRequest(http.MethodGet, "/api/manager/audit/users.csv", nil) req.SetBasicAuth("admin", "pw") rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } body := rec.Body.String() for _, want := range []string{"id,username,role,enabled,system,created_at,last_login_at", "auditor,viewer"} { if !strings.Contains(body, want) { t.Errorf("CSV missing %q:\n%s", want, body) } } if ct := rec.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/csv") { t.Errorf("content-type=%q", ct) } } func TestAuditApiKeysCsv(t *testing.T) { _, mux := newAuditHarness(t) req := httptest.NewRequest(http.MethodGet, "/api/manager/audit/apikeys.csv", nil) req.SetBasicAuth("admin", "pw") rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } body := rec.Body.String() if !strings.Contains(body, "key_prefix,owner,label,scope") { t.Errorf("CSV header missing:\n%s", body) } if !strings.Contains(body, "w4f_") || !strings.Contains(body, "ci-key") { t.Errorf("key row missing:\n%s", body) } } func TestAuditEndpointsNeedAuth(t *testing.T) { _, mux := newAuditHarness(t) for _, path := range []string{"/api/manager/audit/users.csv", "/api/manager/audit/apikeys.csv"} { req := httptest.NewRequest(http.MethodGet, path, nil) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req) if rec.Code != http.StatusUnauthorized { t.Errorf("%s without auth: status=%d want 401", path, rec.Code) } } }