Files
ARTEX/db/findings_test.go
T
Autumn-27 30dc09ae08 feat: 发现列表改为服务端分页 + 全表聚合(去掉 500 上限)
- db.ListFindingsPage:WHERE(严重度/状态/类型)+ ORDER BY(严重度 CASE 或时间)+ LIMIT/OFFSET,返回当前页与匹配总数
- db.FindingStats:全表 COUNT FILTER 聚合(总数/待处理/高/中/低)+ DISTINCT 漏洞类型,供统计卡与类型下拉,不受分页影响
- GET /api/exploration/findings 带 page/limit → 分页对象{items,total,page,page_size};不带 → 保留裸数组(dashboard 汇总,与 intents 端点兼容策略一致)
- 新增 GET /api/exploration/findings/stats
- 筛选/排序全部下推到 SQL,前端不再一次拉全量再本地切片
- 资产按当前页所需批量解析;per-task 视图经 node_id 关联独立行,复用同一状态并补齐绑定资产
- 前端:发现页改为服务端驱动分页/筛选/排序,统计卡与类型下拉走 stats 接口
- db 层加 findings 分页/筛选/聚合测试(隔离标记 vulnclass,自清理)
2026-08-18 10:05:18 -04:00

100 lines
2.8 KiB
Go

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 5 findings under the marker vulnclass: 3 high, 2 low; 2 pending, 3 resolved.
seed := []struct {
sev, status string
}{
{"high", "pending"},
{"high", "resolved"},
{"high", "resolved"},
{"low", "pending"},
{"low", "resolved"},
}
var ids []int64
for i, s := range seed {
id, err := d.AddFinding(0, 0, vc, 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 5 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 != 5 {
t.Fatalf("total: want 5, got %d", total)
}
if len(p1) != 2 {
t.Fatalf("page1 size: want 2, got %d", len(p1))
}
// severity sort → highs first
if p1[0].Severity != "high" || p1[1].Severity != "high" {
t.Fatalf("severity sort broken: %q, %q", p1[0].Severity, p1[1].Severity)
}
p3, _, err := d.ListFindingsPage(FindingFilter{VulnClass: vc, Sort: "severity"}, 3, 2)
if err != nil {
t.Fatal(err)
}
if len(p3) != 1 {
t.Fatalf("page3 size: want 1 (5 rows / 2), got %d", len(p3))
}
// 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.Total < 5 || 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)
}
}