Report inbox: /api/reports in its own bounded reports.db, dashboard Reports page (list, filter, triage, copy as text)
Build and Push Docker Image / build (push) Failing after 1m20s

This commit is contained in:
Worph
2026-11-04 11:43:01 +00:00
parent cef9dba59e
commit f367ed037f
12 changed files with 1901 additions and 1 deletions
+121
View File
@@ -0,0 +1,121 @@
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) }