package reports import ( "encoding/json" "path/filepath" "strings" "testing" "time" ) func open(t *testing.T, maxRows int, maxBytes int64) *Store { t.Helper() s, err := Open(filepath.Join(t.TempDir(), "db", "reports.db"), maxRows, maxBytes) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = s.Close() }) return s } func envelope(t *testing.T, v map[string]any) map[string]json.RawMessage { t.Helper() b, _ := json.Marshal(v) var out map[string]json.RawMessage if err := json.Unmarshal(b, &out); err != nil { t.Fatal(err) } return out } func file(t *testing.T, s *Store, v map[string]any) int64 { t.Helper() r, err := Normalize(envelope(t, v), time.Now()) if err != nil { t.Fatal(err) } id, err := s.Insert(r) if err != nil { t.Fatal(err) } return id } func TestNormalizeRequiresApp(t *testing.T) { if _, err := Normalize(envelope(t, map[string]any{"note": "x"}), time.Now()); err == nil { t.Fatal("a report without app must be refused") } } func TestNormalizeClampsAndBounds(t *testing.T) { log := make([]string, 250) for i := range log { log[i] = strings.Repeat("é", 700) } now := time.Now() r, err := Normalize(envelope(t, map[string]any{ "app": "meta-read", "note": strings.Repeat("n", 1500), "title": strings.Repeat("t", 300), "log": log, "context": map[string]string{"big": strings.Repeat("x", MaxBlockBytes)}, "reader": map[string]int{"page": 4}, "device": nil, "filedAt": now.Add(-time.Hour).UnixMilli(), }), now) if err != nil { t.Fatal(err) } if len(r.Note) != maxNote || len(r.Title) != maxTitle { t.Fatalf("note/title not clamped: %d %d", len(r.Note), len(r.Title)) } if r.TS != now.Add(-time.Hour).UnixMilli() || r.Received != now.UnixMilli() { t.Fatalf("filedAt should set ts, received stays now: %d %d", r.TS, r.Received) } var body map[string]json.RawMessage _ = json.Unmarshal(r.Body, &body) var lines []string _ = json.Unmarshal(body["log"], &lines) if len(lines) != MaxLogLines || r.LogLines != MaxLogLines { t.Fatalf("log tail: %d lines (col %d)", len(lines), r.LogLines) } if n := len([]rune(lines[0])); n != MaxLogLineChars { t.Fatalf("log line clamped by rune: %d", n) } if !strings.Contains(string(body["context"]), `"truncated":true`) { t.Fatalf("oversized block not replaced by a marker: %.80s", body["context"]) } if string(body["reader"]) != `{"page":4}` { t.Fatalf("app block not kept verbatim: %s", body["reader"]) } if _, ok := body["device"]; ok { t.Fatal("null block should be dropped") } if _, ok := body["filedAt"]; ok { t.Fatal("filedAt belongs in ts, not the body") } } func TestNormalizeIgnoresImplausibleFiledAt(t *testing.T) { now := time.Now() for _, at := range []time.Time{now.Add(-60 * 24 * time.Hour), now.Add(time.Hour)} { r, _ := Normalize(envelope(t, map[string]any{"app": "a", "filedAt": at.UnixMilli()}), now) if r.TS != now.UnixMilli() { t.Fatalf("filedAt %v should be ignored", at) } } } func TestInsertGetListFilters(t *testing.T) { s := open(t, 0, 0) a := file(t, s, map[string]any{"app": "meta-watch", "note": "stall at 3%", "cid": "bafyA"}) b := file(t, s, map[string]any{"app": "meta-read", "note": "blank page", "uid": "u1"}) c := file(t, s, map[string]any{"app": "meta-read", "note": "wrong cover 100%"}) got, err := s.Get(a) if err != nil || got.App != "meta-watch" || got.CID != "bafyA" || got.Status != StatusOpen { t.Fatalf("get: %+v %v", got.Summary, err) } if _, err := s.Get(999); err != ErrNotFound { t.Fatalf("missing id: %v", err) } p, err := s.List(Filter{}) if err != nil || len(p.Reports) != 3 || p.Reports[0].ID != c { t.Fatalf("list newest first: %+v %v", p.Reports, err) } if p.Apps["meta-read"] != 2 || p.Apps["meta-watch"] != 1 || p.Counts[StatusOpen] != 3 { t.Fatalf("counts: %+v %+v", p.Apps, p.Counts) } p, _ = s.List(Filter{App: "meta-read"}) if len(p.Reports) != 2 || p.Counts[StatusOpen] != 2 || p.Apps["meta-watch"] != 1 { t.Fatalf("app filter: %+v counts %+v apps %+v", p.Reports, p.Counts, p.Apps) } p, _ = s.List(Filter{Q: "100%"}) if len(p.Reports) != 1 || p.Reports[0].ID != c { t.Fatalf("q is a literal substring (%% escaped): %+v", p.Reports) } p, _ = s.List(Filter{Q: "u1"}) if len(p.Reports) != 1 || p.Reports[0].ID != b { t.Fatalf("q matches uid: %+v", p.Reports) } p, _ = s.List(Filter{Limit: 2}) if len(p.Reports) != 2 || !p.More { t.Fatalf("page 1: %d more=%v", len(p.Reports), p.More) } p, _ = s.List(Filter{Limit: 2, Before: p.Reports[1].ID}) if len(p.Reports) != 1 || p.More || p.Reports[0].ID != a { t.Fatalf("page 2: %+v more=%v", p.Reports, p.More) } } func TestUpdateAndDelete(t *testing.T) { s := open(t, 0, 0) id := file(t, s, map[string]any{"app": "a"}) st, note := StatusTriaged, " Deck #900 " r, err := s.Update(id, &st, ¬e) if err != nil || r.Status != StatusTriaged || r.TriageNote != "Deck #900" || r.Updated == 0 { t.Fatalf("update: %+v %v", r.Summary, err) } bad := "done" if _, err := s.Update(id, &bad, nil); err == nil { t.Fatal("invalid status accepted") } if _, err := s.Update(42, &st, nil); err != ErrNotFound { t.Fatalf("update missing: %v", err) } p, _ := s.List(Filter{Status: StatusTriaged}) if len(p.Reports) != 1 { t.Fatalf("status filter: %+v", p.Reports) } if err := s.Delete(id); err != nil { t.Fatal(err) } if err := s.Delete(id); err != ErrNotFound { t.Fatalf("second delete: %v", err) } } func TestPruneEvictsClosedThenOldest(t *testing.T) { s := open(t, 3, 0) ids := []int64{ file(t, s, map[string]any{"app": "a", "note": "1"}), file(t, s, map[string]any{"app": "a", "note": "2"}), file(t, s, map[string]any{"app": "a", "note": "3"}), } closed := StatusClosed if _, err := s.Update(ids[2], &closed, nil); err != nil { t.Fatal(err) } // Fourth report: the closed one (newest of the three) goes first. file(t, s, map[string]any{"app": "a", "note": "4"}) if _, err := s.Get(ids[2]); err != ErrNotFound { t.Fatal("closed report should be evicted before older open ones") } // Fifth: no closed left, so the oldest open goes. file(t, s, map[string]any{"app": "a", "note": "5"}) if _, err := s.Get(ids[0]); err != ErrNotFound { t.Fatal("oldest open report should be evicted next") } p, _ := s.List(Filter{}) if len(p.Reports) != 3 { t.Fatalf("store over its row bound: %d", len(p.Reports)) } } func TestPruneByBytes(t *testing.T) { s := open(t, 0, 3000) for i := 0; i < 5; i++ { file(t, s, map[string]any{"app": "a", "note": strings.Repeat("x", 900)}) } p, _ := s.List(Filter{}) var total int64 for _, r := range p.Reports { total += r.Bytes } if total > 3000 || len(p.Reports) == 0 { t.Fatalf("byte bound not held: %d bytes in %d reports", total, len(p.Reports)) } } func TestClampKeepsRunes(t *testing.T) { if got := clamp("héllo", 2); got != "hé" { t.Fatalf("clamp: %q", got) } }