Files
ARTEX/db/finding_retests_test.go
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

243 lines
7.8 KiB
Go

package db
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
"testing"
)
func retestDB(t *testing.T) (*DB, int64) {
t.Helper()
d, err := Open(testDSN(t))
if err != nil {
t.Fatalf("open test database: %v", err)
}
fid, err := d.AddFinding(0, 0, "retest-test", "test vulnerability", "high", "original summary", "original evidence", "worker", nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_, _ = d.Exec(`DELETE FROM conversations WHERE id IN (SELECT conversation_id FROM finding_retests WHERE finding_id=$1)`, fid)
_, _ = d.DeleteFinding(fid)
d.Close()
})
return d, fid
}
func TestRetestAtomicDeduplicationAndHistory(t *testing.T) {
d, fid := retestDB(t)
var wg sync.WaitGroup
var mu sync.Mutex
createdCount := 0
ids := make([]int64, 0, 8)
for range 8 {
wg.Go(func() {
r, c, created, err := d.CreateFindingRetest(t.Context(), fid, " fixed in v2 ")
if err != nil {
t.Error(err)
return
}
mu.Lock()
defer mu.Unlock()
ids = append(ids, r.ID)
if created {
createdCount++
if c == nil {
t.Error("created without conversation")
}
}
})
}
wg.Wait()
if createdCount != 1 || len(ids) != 8 {
t.Fatalf("created=%d ids=%v", createdCount, ids)
}
for _, id := range ids {
if id != ids[0] {
t.Fatal("duplicate active retests", ids)
}
}
rows, err := d.ListFindingRetests(fid)
if err != nil || len(rows) != 1 {
t.Fatalf("history=%v err=%v", rows, err)
}
r := rows[0]
if r.Snapshot != nil {
t.Fatal("history leaks large snapshot")
}
ctx := t.Context()
full, err := d.FindingRetestForConversation(ctx, *r.ConversationID)
if err != nil || !strings.Contains(string(full.Snapshot), "original evidence") {
t.Fatalf("snapshot=%+v err=%v", full, err)
}
var messages int
if err := d.QueryRow(`SELECT count(*) FROM conversation_activities WHERE conversation_id=$1 AND kind='user'`, *r.ConversationID).Scan(&messages); err != nil || messages != 1 {
t.Fatalf("messages=%d err=%v", messages, err)
}
_, _ = d.Exec(`UPDATE findings SET evidence='changed evidence' WHERE id=$1`, fid)
full, err = d.FindingRetestForConversation(ctx, *r.ConversationID)
if err != nil || strings.Contains(string(full.Snapshot), "changed evidence") {
t.Fatal("snapshot changed", err)
}
if err = d.RecordFindingRetestResult(ctx, *r.ConversationID, "fixed", "summary", "proof"); !errors.Is(err, ErrRetestNotRunning) {
t.Fatal("pending accepted result", err)
}
if ok, err := d.StartFindingRetest(ctx, r.ID); err != nil || !ok {
t.Fatalf("start=%t %v", ok, err)
}
if err := d.RecordFindingRetestResult(ctx, 0, "fixed", "summary", "proof"); !errors.Is(err, ErrRetestNotRunning) {
t.Fatal("unscoped write accepted", err)
}
for range 2 {
if err := d.RecordFindingRetestResult(ctx, *r.ConversationID, "fixed", "fix verification passed", "control works normally, original trigger condition no longer applies"); err != nil {
t.Fatal(err)
}
}
if err := d.RecordFindingRetestResult(ctx, *r.ConversationID, "reproduced", "different", "proof"); !errors.Is(err, ErrRetestNotRunning) {
t.Fatal("overwrote staged result", err)
}
if err := d.FinishFindingRetest(r.ID, "completed", ""); err != nil {
t.Fatal(err)
}
if err := d.RecordFindingRetestResult(ctx, *r.ConversationID, "fixed", "summary", "proof"); !errors.Is(err, ErrRetestNotRunning) {
t.Fatal("overwrote sealed result", err)
}
f, _ := d.GetFinding(fid)
if f.Status != FindingFixed || f.Evidence != "changed evidence" || f.Report != "" {
t.Fatal("fixed retest did not update only triage", f)
}
next, _, created, err := d.CreateFindingRetest(ctx, fid, "second")
if err != nil || !created || next.ID == r.ID {
t.Fatalf("new history=%+v created=%t err=%v", next, created, err)
}
if err := d.FinishFindingRetest(next.ID, "completed", ""); err != nil {
t.Fatal(err)
}
rows, _ = d.ListFindingRetests(fid)
if len(rows) != 2 || rows[0].Status != "failed" || rows[0].Error == "" || rows[1].Verdict != "fixed" {
t.Fatal("missing verdict treated as successful", rows)
}
}
func TestRetestDeletionAndRestart(t *testing.T) {
d, fid := retestDB(t)
r, c, _, err := d.CreateFindingRetest(t.Context(), fid, "delete")
if err != nil {
t.Fatal(err)
}
if err := d.DeleteConversation(c.ID); err != nil {
t.Fatal(err)
}
rows, _ := d.ListFindingRetests(fid)
if len(rows) != 1 || rows[0].ConversationID != nil || rows[0].Status != "stopped" {
t.Fatal(rows)
}
next, c2, created, err := d.CreateFindingRetest(t.Context(), fid, "restart")
if err != nil || !created {
t.Fatal("deleted session blocks retry", err)
}
if ok, err := d.StartFindingRetest(t.Context(), next.ID); err != nil || !ok {
t.Fatal(err)
}
if err := d.RecoverFindingRetests(); err != nil {
t.Fatal(err)
}
if err := d.FinishFindingRetest(next.ID, "completed", ""); err != nil {
t.Fatal(err)
}
full, _ := d.FindingRetestForConversation(t.Context(), c2.ID)
if full.Status != "stopped" || full.FinishedAt == nil {
t.Fatal("restart result overwritten", full)
}
if _, err := d.DeleteFinding(fid); err != nil {
t.Fatal(err)
}
var count int
_ = d.QueryRow(`SELECT count(*) FROM finding_retests WHERE id IN ($1,$2)`, r.ID, next.ID).Scan(&count)
if count != 0 {
t.Fatal("finding deletion did not cascade")
}
_ = d.DeleteConversation(c2.ID)
}
func TestRetestValidationAndRollback(t *testing.T) {
d, fid := retestDB(t)
ctx, cancel := context.WithCancel(t.Context())
cancel()
if _, _, _, err := d.CreateFindingRetest(ctx, fid, ""); err == nil {
t.Fatal("cancelled creation succeeded")
}
for _, args := range [][3]string{{"unknown", "summary", "proof"}, {"fixed", " ", "proof"}, {"fixed", "summary", ""}, {"fixed", strings.Repeat("x", 16001), "proof"}} {
if err := d.RecordFindingRetestResult(t.Context(), 0, args[0], args[1], args[2]); err == nil {
t.Fatal("bad result accepted", args[0])
}
}
r, c, _, err := d.CreateFindingRetest(t.Context(), fid, "snapshot")
if err != nil {
t.Fatal(err)
}
full, err := d.FindingRetestForConversation(t.Context(), c.ID)
var snap map[string]json.RawMessage
if err != nil || json.Unmarshal(full.Snapshot, &snap) != nil || len(snap["finding"]) == 0 {
t.Fatal("invalid snapshot", err)
}
if r.Notes != "snapshot" {
t.Fatal("notes lost")
}
}
func TestRetestFixedTriageOnlyAfterSuccessfulCompletion(t *testing.T) {
for _, tc := range []struct{ verdict, terminal, want string }{
{"fixed", "completed", FindingFixed},
{"fixed", "failed", FindingInProgress},
{"fixed", "stopped", FindingInProgress},
{"reproduced", "completed", FindingInProgress},
{"inconclusive", "completed", FindingInProgress},
{"", "completed", FindingInProgress},
} {
t.Run(tc.verdict+"/"+tc.terminal, func(t *testing.T) {
d, fid := retestDB(t)
if _, err := d.SetFindingStatus(fid, FindingInProgress); err != nil {
t.Fatal(err)
}
r, c, _, err := d.CreateFindingRetest(t.Context(), fid, "check triage")
if err != nil {
t.Fatal(err)
}
if _, err := d.StartFindingRetest(t.Context(), r.ID); err != nil {
t.Fatal(err)
}
if tc.verdict != "" {
if err := d.RecordFindingRetestResult(t.Context(), c.ID, tc.verdict, "summary", "proof"); err != nil {
t.Fatal(err)
}
}
f, err := d.GetFinding(fid)
if err != nil || f.Status != FindingInProgress {
t.Fatal("staged verdict changed triage", err, f)
}
if err := d.FinishFindingRetest(r.ID, tc.terminal, ""); err != nil {
t.Fatal(err)
}
f, err = d.GetFinding(fid)
if err != nil || f.Status != tc.want || f.Evidence != "original evidence" {
t.Fatalf("finding=%+v err=%v want=%s", f, err, tc.want)
}
// Re-delivering completion must not undo a later user decision.
if _, err := d.SetFindingStatus(fid, FindingIgnored); err != nil {
t.Fatal(err)
}
if err := d.FinishFindingRetest(r.ID, "completed", ""); err != nil {
t.Fatal(err)
}
f, err = d.GetFinding(fid)
if err != nil || f.Status != FindingIgnored {
t.Fatal("replayed completion overwrote triage", err, f)
}
})
}
}