package httpapi import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "knowledge-governance/internal/jobs" ) func captureRequest(body string) *http.Request { request := httptest.NewRequest(http.MethodPost, "/api/v1/library/captures", strings.NewReader(body)) request.Header.Set("Content-Type", "application/json") return request } func TestLibraryCaptureEnqueuesFixedJobShape(t *testing.T) { store := &fakeJobStore{enqueueEchoRequestedID: true} recorder := httptest.NewRecorder() libraryCaptureEnqueue(recorder, captureRequest(`{"scope":"external","job":"lib-external-review"}`), captureReadyConfig(store)) if recorder.Code != http.StatusAccepted { t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) } enqueued := store.enqueued if enqueued.Type != libraryCaptureJobType { t.Fatalf("job type %q", enqueued.Type) } if enqueued.IdempotencyKey != "library.capture:lib-external-review" { t.Fatalf("idempotency key %q", enqueued.IdempotencyKey) } var payload map[string]string if err := json.Unmarshal(enqueued.Payload, &payload); err != nil { t.Fatal(err) } if payload["scope"] != "external" || payload["capture_job"] != "lib-external-review" { t.Fatalf("payload %v", payload) } var body map[string]any if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body["capture_job"] != "lib-external-review" || body["scope"] != "external" { t.Fatalf("response %v", body) } } func TestLibraryCaptureRejectsInvalidRequest(t *testing.T) { for _, body := range []string{ `{"scope":"production"}`, `{"scope":"internal","job":"bad job; rm -rf /"}`, `not json`, } { store := &fakeJobStore{} recorder := httptest.NewRecorder() libraryCaptureEnqueue(recorder, captureRequest(body), captureReadyConfig(store)) if recorder.Code != http.StatusBadRequest { t.Fatalf("body %q status=%d", body, recorder.Code) } if store.enqueued.Type != "" { t.Fatalf("invalid request %q still enqueued", body) } } } func TestLibraryCaptureQueueIDFitsTheColumn(t *testing.T) { // jobs.id 是 varchar(36):拼长的 id 会在真实 PG 上 500(value too long)。 // 页面长期停在"未接线",这条从没被真实请求覆盖过,所以用测试锁住列宽。 for _, scope := range []string{"internal", "external"} { store := &fakeJobStore{enqueueEchoRequestedID: true} recorder := httptest.NewRecorder() libraryCaptureEnqueue(recorder, captureRequest(`{"scope":"`+scope+`"}`), captureReadyConfig(store)) if recorder.Code != http.StatusAccepted { t.Fatalf("%s status=%d body=%s", scope, recorder.Code, recorder.Body.String()) } enqueued := store.enqueued if len(enqueued.ID) > 36 { t.Fatalf("%s 队列作业 id 超长(%d):%q", scope, len(enqueued.ID), enqueued.ID) } if !strings.HasPrefix(enqueued.ID, "libcap-"+scope+"-") { t.Fatalf("%s 队列作业 id 形状变了:%q", scope, enqueued.ID) } } } func TestLibraryCaptureJobsAsksStoreForItsOwnType(t *testing.T) { // The global queue is busy with unrelated traffic; the capture job must still // be visible because the query is type-scoped, not filtered after the fact. store := &fakeJobStore{ listResult: []jobs.Job{{ID: "busy", Type: "artifact.fetch", State: "running"}}, listByTypeResult: []jobs.Job{ {ID: "a", Type: libraryCaptureJobType, State: "completed", Payload: json.RawMessage(`{"scope":"internal","capture_job":"lib-internal-1","requested_by":"yaozhanghao"}`), CreatedAt: time.Date(2026, 10, 28, 3, 0, 0, 0, time.UTC)}, }, } recorder := httptest.NewRecorder() libraryCaptureJobs(recorder, httptest.NewRequest(http.MethodGet, "/api/v1/library/captures", nil), Config{Store: store}) if recorder.Code != http.StatusOK { t.Fatalf("status=%d", recorder.Code) } if store.listType != libraryCaptureJobType { t.Fatalf("store queried with type %q", store.listType) } var body struct { Available bool `json:"available"` Jobs []map[string]any `json:"jobs"` } if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatal(err) } if !body.Available || len(body.Jobs) != 1 || body.Jobs[0]["capture_job"] != "lib-internal-1" { t.Fatalf("unexpected body %+v", body) } } // Reusing a capture job id with another scope must be refused, and the response // must report the scope that was actually stored. func TestLibraryCaptureRejectsScopeConflictForReusedJob(t *testing.T) { store := &fakeJobStore{enqueueResult: &jobs.Job{ID: "existing-job", Type: libraryCaptureJobType, State: jobs.StateCompleted, Payload: json.RawMessage(`{"scope":"internal","capture_job":"lib-shared","requested_by":"yaozhanghao"}`)}} recorder := httptest.NewRecorder() libraryCaptureEnqueue(recorder, captureRequest(`{"scope":"external","job":"lib-shared"}`), captureReadyConfig(store)) if recorder.Code != http.StatusConflict { t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) } var body map[string]any if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body["code"] != "library_capture_scope_conflict" || body["existing_scope"] != "internal" || body["requested_scope"] != "external" { t.Fatalf("unexpected conflict body %v", body) } } // A reused job id with an identical request is answered from the stored row. func TestLibraryCaptureReplayReportsStoredJob(t *testing.T) { store := &fakeJobStore{enqueueResult: &jobs.Job{ID: "existing-job", Type: libraryCaptureJobType, State: jobs.StateCompleted, Payload: json.RawMessage(`{"scope":"internal","capture_job":"lib-shared","requested_by":"yaozhanghao"}`)}} recorder := httptest.NewRecorder() libraryCaptureEnqueue(recorder, captureRequest(`{"scope":"internal","job":"lib-shared"}`), captureReadyConfig(store)) if recorder.Code != http.StatusAccepted { t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) } var body map[string]any if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body["scope"] != "internal" || body["capture_job"] != "lib-shared" || body["reused"] != true || body["job_id"] != "existing-job" { t.Fatalf("unexpected replay body %v", body) } }