ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
477 lines
17 KiB
Go
477 lines
17 KiB
Go
package server
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Autumn-27/artex/agent"
|
|
"github.com/Autumn-27/artex/db"
|
|
"github.com/Autumn-27/artex/traffic"
|
|
)
|
|
|
|
func trafficEvidenceServer(t *testing.T) (*Server, *db.RecordedFinding, func(string, string, string) *httptest.ResponseRecorder) {
|
|
t.Helper()
|
|
m, err := NewManager(t.TempDir(), "")
|
|
if err != nil {
|
|
t.Skipf("postgres unavailable (%v) — skipping", err)
|
|
}
|
|
t.Cleanup(func() { m.Close() })
|
|
m.traffic, err = traffic.Open(filepath.Join(m.dir, "traffic"), ":0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
task, err := m.CreateTask("traffic evidence API", "local evidence", nil, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tid, _ := strconv.ParseInt(task.ID, 10, 64)
|
|
t.Cleanup(func() { m.pg.Exec(`DELETE FROM task_archives WHERE task_id=$1`, tid); m.pg.DeleteTask(tid) })
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
s := New(ctx, m, t.TempDir(), t.TempDir(), t.TempDir())
|
|
s.archiveWG.Wait()
|
|
// The archive test below replaces the cancelled service context. Wait for
|
|
// the side-question snapshot writer too before reusing this fixture.
|
|
<-s.side.done
|
|
token, err := signJWT(s.jwtKey)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := func(method, path, body string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
r := httptest.NewRequest(method, path, strings.NewReader(body))
|
|
r.Header.Set("Authorization", "Bearer "+token)
|
|
r.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
s.Handler().ServeHTTP(w, r)
|
|
return w
|
|
}
|
|
input := db.RecordFindingInput{TaskID: tid, ExplorationID: task.ExpID, Worker: "test", VulnClass: "TEST", Summary: "bound evidence", Name: "Evidence API", Severity: "low"}
|
|
f, err := s.evidenceStore().Record(context.Background(), input, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return s, f, request
|
|
}
|
|
|
|
func seedServerEvidenceFlow(t *testing.T, s *Server, id string, body []byte) {
|
|
t.Helper()
|
|
_, err := s.m.traffic.DB().Exec(`INSERT INTO exchanges(id,ts,host,method,url_template,url,status,content_type,req_len,resp_len,path) VALUES(?,1,'evidence.local','POST','/','http://evidence.local/',200,'application/octet-stream',0,?,'')`, id, len(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = s.m.traffic.DB().Exec(`INSERT INTO exchange_bodies(id,req_head,resp_head,req_body,resp_body) VALUES(?,'POST / HTTP/1.1','HTTP/1.1 200 OK',?,?)`, id, []byte{}, body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestFindingTrafficToolUpgradePreservesCustomization(t *testing.T) {
|
|
s, _, _ := trafficEvidenceServer(t)
|
|
pg := s.m.pg
|
|
worker, err := pg.GetAgentByKey("worker")
|
|
if err != nil || worker == nil {
|
|
t.Fatal("missing worker", err)
|
|
}
|
|
oldPrompt, _ := pg.CurrentPrompt(worker.ID)
|
|
t.Cleanup(func() { pg.SavePrompt(worker.ID, oldPrompt, "restore test fixture", "test") })
|
|
if _, err := pg.SavePrompt(worker.ID, "USER CUSTOM PROMPT", "test", "test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
custom := json.RawMessage(`{"type":"object","properties":{"evidence":{"type":"string","description":"user evidence instructions"}},"required":["evidence"]}`)
|
|
for _, key := range []string{"report_finding", "update_finding_report", "get_finding_traffic"} {
|
|
old, err := pg.GetTool(key)
|
|
if err != nil || old == nil {
|
|
t.Fatal("missing tool", key, err)
|
|
}
|
|
t.Cleanup(func() {
|
|
bindings, _ := json.Marshal(old.Agents)
|
|
pg.UpdateTool(old.Key, old.Description, old.Schema, bindings, old.Enabled)
|
|
})
|
|
if err := pg.UpdateTool(key, "user description", custom, json.RawMessage(`["worker"]`), false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := pg.SetSetting("finding_traffic_tools_v1", "false"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.seedFindingTrafficTools()
|
|
for key, property := range map[string]string{"report_finding": "traffic_refs", "update_finding_report": "evidence_version"} {
|
|
tool, err := pg.GetTool(key)
|
|
if err != nil || tool == nil {
|
|
t.Fatal(err)
|
|
}
|
|
var schema struct {
|
|
Properties map[string]json.RawMessage `json:"properties"`
|
|
Required []string `json:"required"`
|
|
}
|
|
if err := json.Unmarshal(tool.Schema, &schema); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if tool.Enabled || tool.Description != "user description" || len(tool.Agents) != 1 || tool.Agents[0] != "worker" || len(schema.Required) != 1 || schema.Required[0] != "evidence" || len(schema.Properties[property]) == 0 || !strings.Contains(string(schema.Properties["evidence"]), "user evidence instructions") {
|
|
t.Fatalf("custom configuration overwritten: %+v", tool)
|
|
}
|
|
}
|
|
reader, _ := pg.GetTool("get_finding_traffic")
|
|
if reader.Enabled || !contains(reader.Agents, "reporter") {
|
|
t.Fatal("reader should be bound without enabling it")
|
|
}
|
|
if err := pg.RemoveAgentFromTool("reporter", "get_finding_traffic"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s.seedFindingTrafficTools()
|
|
reader, _ = pg.GetTool("get_finding_traffic")
|
|
if contains(reader.Agents, "reporter") {
|
|
t.Fatal("restart overwrote user's unbinding")
|
|
}
|
|
if got, _ := pg.CurrentPrompt(worker.ID); got != "USER CUSTOM PROMPT" {
|
|
t.Fatal("custom prompt replaced")
|
|
}
|
|
}
|
|
|
|
func TestFindingTrafficAPIAndExport(t *testing.T) {
|
|
s, f, req := trafficEvidenceServer(t)
|
|
body := bytes.Repeat([]byte{0, 255, 65, 66}, 5000)
|
|
for _, id := range []string{"a", "b", "c"} {
|
|
seedServerEvidenceFlow(t, s, id, body)
|
|
}
|
|
base := fmt.Sprintf("/api/exploration/findings/%d/traffic", f.FindingID)
|
|
w := req("POST", base, `{"traffic_refs":[{"traffic_id":"a","role":"baseline"},{"traffic_id":"b","role":"proof","note":"proof note"},{"traffic_id":"c"}]}`)
|
|
if w.Code != 200 {
|
|
t.Fatalf("bind: %d %s", w.Code, w.Body)
|
|
}
|
|
var list db.FindingTraffic
|
|
if err := json.Unmarshal(w.Body.Bytes(), &list); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(list.Bindings) != 3 || list.Bindings[0].Snapshot.ReqHead != "" {
|
|
t.Fatal("not a bounded summary")
|
|
}
|
|
first, second, third := list.Bindings[0].ID, list.Bindings[1].ID, list.Bindings[2].ID
|
|
if w = req("PATCH", fmt.Sprintf("%s/%d", base, first), `{"version":0,"note":"stale"}`); w.Code != 409 {
|
|
t.Fatal(w.Code, w.Body)
|
|
}
|
|
if w = req("PUT", base+"/order", fmt.Sprintf(`{"version":1,"binding_ids":["%d","%d","%d"]}`, third, first, second)); w.Code != 200 {
|
|
t.Fatal(w.Code, w.Body)
|
|
}
|
|
if w = req("PUT", base+"/order", fmt.Sprintf(`{"version":2,"binding_ids":["%d"]}`, first)); w.Code != 409 {
|
|
t.Fatal(w.Code, w.Body)
|
|
}
|
|
if w = req("GET", fmt.Sprintf("%s/%d", base, first), ""); w.Code != 200 || !strings.Contains(w.Body.String(), `"binary":true`) {
|
|
t.Fatal(w.Code, w.Body)
|
|
}
|
|
if w = req("GET", fmt.Sprintf("%s/%d/body?side=response&offset=20001", base, first), ""); w.Code != 422 {
|
|
t.Fatal(w.Code, w.Body)
|
|
}
|
|
if _, err := s.m.traffic.DeleteHost("evidence.local"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if w = req("GET", fmt.Sprintf("%s/%d/body?side=response&download=1", base, first), ""); w.Code != 200 || !bytes.Equal(w.Body.Bytes(), body) {
|
|
t.Fatal("download after original removal failed")
|
|
}
|
|
// IDs belong to the requested finding; cross-finding detail cannot be read.
|
|
finding, err := s.m.pg.GetFinding(f.FindingID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
task, err := s.m.pg.GetTask(*finding.TaskID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
other, err := s.evidenceStore().Record(t.Context(), db.RecordFindingInput{TaskID: task.ID, ExplorationID: task.ExplorationID, Summary: "other", Severity: "low"}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if w = req("GET", fmt.Sprintf("/api/exploration/findings/%d/traffic/%d", other.FindingID, first), ""); w.Code != 404 {
|
|
t.Fatal(w.Code, w.Body)
|
|
}
|
|
exportURL := fmt.Sprintf("/api/exploration/findings/export?format=md-zip&scope=selected&ids=%d", f.FindingID)
|
|
w = req("GET", exportURL, "")
|
|
if w.Code != 200 {
|
|
t.Fatal(w.Code, w.Body)
|
|
}
|
|
archive, err := zip.NewReader(bytes.NewReader(w.Body.Bytes()), int64(w.Body.Len()))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
binaries := 0
|
|
linked := false
|
|
for _, entry := range archive.File {
|
|
r, err := entry.Open()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := io.ReadAll(r)
|
|
r.Close()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.HasSuffix(entry.Name, "/response.bin") {
|
|
binaries++
|
|
if !bytes.Equal(raw, body) {
|
|
t.Fatal("export changed binary")
|
|
}
|
|
}
|
|
if strings.HasSuffix(entry.Name, ".md") && bytes.Contains(raw, []byte(fmt.Sprintf("evidence/%d/%d/", f.FindingID, third))) {
|
|
linked = true
|
|
}
|
|
}
|
|
if binaries != 3 || !linked {
|
|
t.Fatalf("missing attachments/links: %d %v", binaries, linked)
|
|
}
|
|
// Reports can use snapshots with capture stopped and original rows gone.
|
|
result, err := s.toolGetFindingTraffic().Call(context.Background(), json.RawMessage(fmt.Sprintf(`{"finding_id":"%d"}`, f.FindingID)), nil)
|
|
if err != nil || !strings.Contains(result.Flatten(), "proof note") {
|
|
t.Fatal(result, err)
|
|
}
|
|
// Corrupt body must fail before an attachment response is sent.
|
|
snap := list.Bindings[0].Snapshot
|
|
path := filepath.Join(s.m.dir, "evidence", "blobs", snap.RespHash[:2], snap.RespHash+".bin")
|
|
if err = os.WriteFile(path, []byte("corrupt"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if w = req("GET", exportURL, ""); w.Code < 400 || strings.Contains(w.Header().Get("Content-Type"), "zip") {
|
|
t.Fatal("corrupt export returned an attachment", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestFindingTrafficArchiveV3RoundTripAndRetry(t *testing.T) {
|
|
s, f, _ := trafficEvidenceServer(t)
|
|
ctx := context.Background()
|
|
s.ctx = ctx
|
|
body := []byte("evidence without original traffic\x00\xff")
|
|
seedServerEvidenceFlow(t, s, "original", body)
|
|
list, err := s.evidenceStore().Bind(ctx, f.FindingID, []db.TrafficRef{{TrafficID: "original", Role: "proof", Note: "keep note"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
finding, err := s.m.pg.GetFinding(f.FindingID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tid := *finding.TaskID
|
|
sharedTask, err := s.m.CreateTask("shared evidence", "fixture", nil, 0, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sid, _ := strconv.ParseInt(sharedTask.ID, 10, 64)
|
|
defer s.m.pg.DeleteTask(sid)
|
|
shared, err := s.evidenceStore().Record(ctx, db.RecordFindingInput{TaskID: sid, ExplorationID: sharedTask.ExpID, Summary: "shared", Severity: "low"}, []db.TrafficRef{{TrafficID: "original"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = s.m.traffic.DeleteHost("evidence.local"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = s.m.SetTaskPaused(strconv.FormatInt(tid, 10), true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
job, err := s.m.pg.QueueTaskArchive(tid)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = s.m.pg.EditFindingTraffic(ctx, f.FindingID, list.Bindings[0].ID, 1, nil, nil, true, nil); err == nil {
|
|
t.Fatal("write allowed after archive queued")
|
|
}
|
|
if err = s.runOneTaskArchiveJob(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, _ := s.m.pg.GetFinding(f.FindingID); got != nil {
|
|
t.Fatal("archive retained hot finding")
|
|
}
|
|
if err = s.evidenceStore().Collect(ctx, time.Now().Add(48*time.Hour)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = s.evidenceStore().WithBinding(ctx, shared.FindingID, shared.Traffic.Bindings[0].ID, func(b db.FindingTrafficBinding) error {
|
|
r, _, err := s.evidenceStore().OpenBody(b.Snapshot, "response")
|
|
if r != nil {
|
|
r.Close()
|
|
}
|
|
return err
|
|
}); err != nil {
|
|
t.Fatal("shared body deleted", err)
|
|
}
|
|
if _, err = s.m.pg.QueueTaskArchiveRestore(job.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Force a database error after validated body installation; then retry normally.
|
|
if _, err = s.m.pg.Exec(`CREATE FUNCTION fail_evidence_restore_test() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'restore fixture failure'; END $$`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = s.m.pg.Exec(`CREATE TRIGGER fail_evidence_restore_test BEFORE INSERT ON finding_traffic_bindings FOR EACH ROW EXECUTE FUNCTION fail_evidence_restore_test()`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer s.m.pg.Exec(`DROP FUNCTION IF EXISTS fail_evidence_restore_test() CASCADE`)
|
|
if err = s.runOneTaskArchiveJob(); err == nil {
|
|
t.Fatal("expected restore failure")
|
|
}
|
|
if got, _ := s.m.pg.GetFinding(f.FindingID); got != nil {
|
|
t.Fatal("partial restore")
|
|
}
|
|
if _, err = s.m.pg.Exec(`DROP FUNCTION fail_evidence_restore_test() CASCADE`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = s.m.pg.QueueTaskArchiveRestore(job.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = s.runOneTaskArchiveJob(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
restored, err := s.m.pg.GetFindingTraffic(ctx, f.FindingID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b := restored.Bindings[0]
|
|
if restored.Version != list.Version || b.ID != list.Bindings[0].ID || b.Note != "keep note" || b.Role != "proof" || b.Position != 0 {
|
|
t.Fatalf("restored: %+v", restored)
|
|
}
|
|
if err = s.evidenceStore().WithBinding(ctx, f.FindingID, b.ID, func(b db.FindingTrafficBinding) error {
|
|
r, _, err := s.evidenceStore().OpenBody(b.Snapshot, "response")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
got, err := io.ReadAll(r)
|
|
if !bytes.Equal(got, body) {
|
|
t.Fatal("restore changed body")
|
|
}
|
|
return err
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The read tool enforces task visibility too.
|
|
result, err := s.toolGetFindingTraffic().Call(agent.WithRunInfo(ctx, agent.RunInfo{TaskID: sid}), json.RawMessage(fmt.Sprintf(`{"finding_id":"%d"}`, f.FindingID)), nil)
|
|
if err != nil || !strings.Contains(result.Flatten(), "不可读取") {
|
|
t.Fatal(result, err)
|
|
}
|
|
}
|
|
|
|
func TestFindingTrafficFailedReportDoesNotTrigger(t *testing.T) {
|
|
s, f, _ := trafficEvidenceServer(t)
|
|
finding, err := s.m.pg.GetFinding(f.FindingID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
task, err := s.m.pg.GetTask(*finding.TaskID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var last int64
|
|
if err = s.m.pg.QueryRow(`SELECT COALESCE(max(id),0) FROM activity`).Scan(&last); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = s.m.pg.SetSchedState(schedKeyLastToolCall, strconv.FormatInt(last, 10)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id, err := s.m.pg.Exploration(task.ExplorationID).AppendActivity(db.Activity{Kind: "tool_result", Tool: "report_finding", ToolUseID: "failure-fixture", IsError: true, Detail: "body missing"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// A nil server makes any attempted trigger fail this test immediately.
|
|
scheduler := &Scheduler{pg: s.m.pg}
|
|
scheduler.fireToolCalls([]*db.AgentTrigger{{OnToolCall: true, ToolNames: []string{"report_finding"}, AgentKey: "reporter"}})
|
|
if got := scheduler.mustState(schedKeyLastToolCall); got != strconv.FormatInt(id, 10) {
|
|
t.Fatal("failed finding must still advance watermark", got)
|
|
}
|
|
}
|
|
|
|
func TestFindingTrafficUTF8SegmentsAndInheritedWrites(t *testing.T) {
|
|
s, f, req := trafficEvidenceServer(t)
|
|
ctx := context.Background()
|
|
text := strings.Repeat("中文证据🙂", 2000)
|
|
seedServerEvidenceFlow(t, s, "unicode", []byte(text))
|
|
list, err := s.evidenceStore().Bind(ctx, f.FindingID, []db.TrafficRef{{TrafficID: "unicode"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var joined strings.Builder
|
|
offset := int64(0)
|
|
for {
|
|
var preview evidencePreview
|
|
err = s.evidenceStore().WithBinding(ctx, f.FindingID, list.Bindings[0].ID, func(b db.FindingTrafficBinding) error {
|
|
var e error
|
|
preview, e = readEvidencePreview(s.evidenceStore(), b.Snapshot, "response", offset, 8192)
|
|
return e
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if preview.Binary {
|
|
t.Fatal("valid UTF-8 classified binary")
|
|
}
|
|
joined.WriteString(preview.Content)
|
|
if !preview.Truncated {
|
|
break
|
|
}
|
|
if preview.NextOffset <= offset {
|
|
t.Fatal("preview made no progress")
|
|
}
|
|
offset = preview.NextOffset
|
|
}
|
|
if joined.String() != text {
|
|
t.Fatal("UTF-8 character split damaged preview")
|
|
}
|
|
finding, err := s.m.pg.GetFinding(f.FindingID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
child, err := s.m.CreateTaskWithOptions("inherited evidence API", "fixture", db.TaskCreateOptions{SourceTaskIDs: []int64{*finding.TaskID}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cid, _ := strconv.ParseInt(child.ID, 10, 64)
|
|
defer s.m.pg.DeleteTask(cid)
|
|
base := fmt.Sprintf("/api/exploration/findings/%d/traffic", f.FindingID)
|
|
suffix := "?context_task=" + child.ID
|
|
if w := req("GET", base+suffix, ""); w.Code != 200 {
|
|
t.Fatal(w.Code, w.Body)
|
|
}
|
|
for _, method := range []string{"POST", "PATCH", "PUT", "DELETE"} {
|
|
path := base
|
|
if method == "PATCH" || method == "DELETE" {
|
|
path += fmt.Sprintf("/%d", list.Bindings[0].ID)
|
|
}
|
|
if method == "PUT" {
|
|
path += "/order"
|
|
}
|
|
w := req(method, path+suffix, `{"version":1,"traffic_refs":[{"traffic_id":"unicode"}]}`)
|
|
if w.Code != 403 {
|
|
t.Fatal(method, w.Code, w.Body)
|
|
}
|
|
}
|
|
if _, err = s.m.traffic.DeleteHost("evidence.local"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result, err := s.toolUpdateFindingReport().Call(ctx, json.RawMessage(fmt.Sprintf(`{"finding_id":%d,"evidence_version":1,"report":"## 证据报告\n\n证据 #%d:已验证完整请求响应"}`, f.NodeID, list.Bindings[0].ID)), nil)
|
|
if err != nil || result.IsError {
|
|
t.Fatal(result, err)
|
|
}
|
|
updated, err := s.m.pg.GetFinding(f.FindingID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if updated.ReportEvidenceVersion != 1 || updated.EvidenceVersion != 1 {
|
|
t.Fatal("report did not record the read evidence version")
|
|
}
|
|
for _, format := range []string{"md-single", "json", "csv"} {
|
|
w := req("GET", fmt.Sprintf("/api/exploration/findings/export?scope=selected&ids=%d&format=%s", f.FindingID, format), "")
|
|
if w.Code != 200 {
|
|
t.Fatal(format, w.Code, w.Body)
|
|
}
|
|
if !strings.Contains(w.Body.String(), fmt.Sprint(list.Bindings[0].ID)) {
|
|
t.Fatal("export omitted evidence ID", format)
|
|
}
|
|
}
|
|
}
|