package db import ( "slices" "testing" ) // TestFindingsPageAndStats exercises ListFindingsPage (filter/sort/paging) and // FindingStats against the live dev PG. It tags its rows with a unique vulnclass // so assertions are isolated from any pre-existing data, and cleans up after. func TestFindingsPageAndStats(t *testing.T) { d, err := Open(testDSN(t)) if err != nil { t.Skipf("postgres unavailable (%v) — skipping", err) } defer d.Close() const vc = "__test_vc_pagination__" // clean any leftovers from a prior aborted run, and clean up on exit cleanup := func() { _, _ = d.Exec(`DELETE FROM findings WHERE vulnclass=$1`, vc) } cleanup() defer cleanup() // Seed 6 findings under the marker vulnclass: 1 critical, 3 high, 2 low; 2 pending. // The critical row carries a name to verify round-trip. seed := []struct { sev, status, name string }{ {"critical", "pending", "严重漏洞标题"}, {"high", "resolved", ""}, {"high", "resolved", ""}, {"high", "pending", ""}, {"low", "resolved", ""}, {"low", "resolved", ""}, } var ids []int64 for i, s := range seed { id, err := d.AddFinding(0, 0, vc, s.name, s.sev, "summary", "poc", "tester", nil) if err != nil { t.Fatalf("AddFinding[%d]: %v", i, err) } if _, err := d.SetFindingStatus(id, s.status); err != nil { t.Fatalf("SetFindingStatus[%d]: %v", i, err) } ids = append(ids, id) } // Filter by our vulnclass → exactly the 6 seeded rows, paged 2 per page. p1, total, err := d.ListFindingsPage(FindingFilter{VulnClass: vc, Sort: "severity"}, 1, 2) if err != nil { t.Fatal(err) } if total != 6 { t.Fatalf("total: want 6, got %d", total) } if len(p1) != 2 { t.Fatalf("page1 size: want 2, got %d", len(p1)) } // severity sort → critical first (with its name round-tripped), then high. if p1[0].Severity != "critical" { t.Fatalf("severity sort: want critical first, got %q", p1[0].Severity) } if p1[0].Name != "严重漏洞标题" { t.Fatalf("name round-trip: want 严重漏洞标题, got %q", p1[0].Name) } if p1[1].Severity != "high" { t.Fatalf("severity sort: want high second, got %q", p1[1].Severity) } // Combined filter: vulnclass + status=pending → 2 rows. pend, total, err := d.ListFindingsPage(FindingFilter{VulnClass: vc, Status: FindingPending}, 1, 50) if err != nil { t.Fatal(err) } if total != 2 || len(pend) != 2 { t.Fatalf("pending filter: want 2/2, got %d/%d", total, len(pend)) } // Combined filter: vulnclass + severity=high → 3 rows. _, total, err = d.ListFindingsPage(FindingFilter{VulnClass: vc, Severity: "high"}, 1, 50) if err != nil { t.Fatal(err) } if total != 3 { t.Fatalf("high filter: want 3, got %d", total) } // Stats: whole-table, so assert our contribution is reflected (>=) and the // marker vulnclass is present. st, err := d.FindingStats() if err != nil { t.Fatal(err) } if st.Critical < 1 { t.Fatalf("stats critical undercount: %+v", st) } if st.Total < 6 || st.High < 3 || st.Low < 2 || st.Pending < 2 { t.Fatalf("stats undercount: %+v", st) } if !slices.Contains(st.VulnClasses, vc) { t.Fatalf("stats vulnclasses missing %q", vc) } // GetFinding: single-row fetch round-trips id/name/severity. one, err := d.GetFinding(ids[0]) if err != nil { t.Fatal(err) } if one == nil || one.ID != ids[0] || one.Severity != "critical" || one.Name != "严重漏洞标题" { t.Fatalf("GetFinding mismatch: %+v", one) } if one.Report != "" { t.Fatalf("new finding report should be empty, got %q", one.Report) } // report column round-trips through GetFinding. if _, err := d.Exec(`UPDATE findings SET report=$1 WHERE id=$2`, "# 报告\n正文", ids[0]); err != nil { t.Fatal(err) } if one, _ = d.GetFinding(ids[0]); one.Report != "# 报告\n正文" { t.Fatalf("report not read back: %q", one.Report) } if miss, err := d.GetFinding(-1); err != nil || miss != nil { t.Fatalf("GetFinding(-1): want nil,nil got %+v,%v", miss, err) } // SetFindingSeverity: standalone row updates; 0 rows for unknown id. if n, err := d.SetFindingSeverity(ids[0], "high"); err != nil || n != 1 { t.Fatalf("SetFindingSeverity: want 1,nil got %d,%v", n, err) } one, _ = d.GetFinding(ids[0]) if one.Severity != "high" { t.Fatalf("severity not updated: %q", one.Severity) } if n, err := d.SetFindingSeverity(-1, "low"); err != nil || n != 0 { t.Fatalf("SetFindingSeverity(-1): want 0,nil got %d,%v", n, err) } }