package api import ( "bytes" "net/http" "net/http/httptest" "path/filepath" "strconv" "strings" "testing" "github.com/gorilla/mux" "github.com/metazla/meta-core/internal/reports" ) func newReportServer(t *testing.T) *Server { t.Helper() store, err := reports.Open(filepath.Join(t.TempDir(), "reports.db"), 0, 0) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = store.Close() }) s := &Server{router: mux.NewRouter(), reports: store} s.setupReportRoutes() return s } func TestReportsRoundTrip(t *testing.T) { s := newReportServer(t) rec, out := do(t, s, "POST", "/api/reports", map[string]any{ "app": "meta-read", "note": "blank page", "uid": "u1", "reader": map[string]any{"page": 3}, "log": []string{"a", "b"}, }) if rec.Code != http.StatusCreated || out["id"] == nil { t.Fatalf("post: %d %s", rec.Code, rec.Body) } id := int64(out["id"].(float64)) rec, out = do(t, s, "GET", "/api/reports", nil) list, _ := out["reports"].([]any) if rec.Code != 200 || len(list) != 1 { t.Fatalf("list: %d %s", rec.Code, rec.Body) } first := list[0].(map[string]any) if _, hasBody := first["body"]; hasBody || first["log_lines"].(float64) != 2 { t.Fatalf("summary should carry log_lines, not the body: %v", first) } path := "/api/reports/" + itoa(id) rec, out = do(t, s, "GET", path, nil) body, _ := out["body"].(map[string]any) if rec.Code != 200 || body["reader"] == nil || out["uid"] != "u1" { t.Fatalf("get: %d %s", rec.Code, rec.Body) } rec, out = do(t, s, "PATCH", path, map[string]any{"status": "triaged", "triage_note": "Deck #1"}) if rec.Code != 200 || out["status"] != "triaged" || out["triage_note"] != "Deck #1" { t.Fatalf("patch: %d %s", rec.Code, rec.Body) } rec, _ = do(t, s, "GET", "/api/reports?status=triaged&app=meta-read", nil) if rec.Code != 200 || !strings.Contains(rec.Body.String(), `"triaged":1`) { t.Fatalf("filtered list: %d %s", rec.Code, rec.Body) } if rec, _ = do(t, s, "DELETE", path, nil); rec.Code != http.StatusNoContent { t.Fatalf("delete: %d", rec.Code) } if rec, _ = do(t, s, "GET", path, nil); rec.Code != http.StatusNotFound { t.Fatalf("get after delete: %d", rec.Code) } } func TestReportsRejects(t *testing.T) { s := newReportServer(t) cases := []struct { name, method, path string body any want int }{ {"no app", "POST", "/api/reports", map[string]any{"note": "x"}, 400}, {"bad status filter", "GET", "/api/reports?status=done", nil, 400}, {"patch nothing", "PATCH", "/api/reports/1", map[string]any{}, 400}, {"patch bad status", "PATCH", "/api/reports/1", map[string]any{"status": "done"}, 400}, {"patch missing", "PATCH", "/api/reports/1", map[string]any{"status": "closed"}, 404}, {"delete missing", "DELETE", "/api/reports/1", nil, 404}, } for _, c := range cases { if rec, _ := do(t, s, c.method, c.path, c.body); rec.Code != c.want { t.Errorf("%s: got %d want %d (%s)", c.name, rec.Code, c.want, rec.Body) } } // Not a JSON object. req := httptest.NewRequest("POST", "/api/reports", strings.NewReader(`[1,2]`)) rec := httptest.NewRecorder() s.router.ServeHTTP(rec, req) if rec.Code != 400 { t.Errorf("array body: %d", rec.Code) } // Over the body cap. big := bytes.Repeat([]byte("x"), maxReportBody+1) req = httptest.NewRequest("POST", "/api/reports", strings.NewReader(`{"app":"a","note":"`+string(big)+`"}`)) rec = httptest.NewRecorder() s.router.ServeHTTP(rec, req) if rec.Code != http.StatusRequestEntityTooLarge { t.Errorf("oversized body: %d", rec.Code) } } func TestReportsUnavailableWithoutStore(t *testing.T) { s := &Server{router: mux.NewRouter()} s.setupReportRoutes() if rec, _ := do(t, s, "GET", "/api/reports", nil); rec.Code != http.StatusServiceUnavailable { t.Fatalf("no store: %d", rec.Code) } } func itoa(n int64) string { return strconv.FormatInt(n, 10) }