package webhook import ( "context" "encoding/json" "errors" "os" "path/filepath" "runtime" "testing" "time" "git.nianxx.cn/wangxuming/NianAIGC/backend/internal/jobs" ) func TestBodyAndSignatureMatchSharedFixture(t *testing.T) { fixture := loadWebhookFixture(t) updatedAt, err := time.Parse(time.RFC3339Nano, fixture.Job.UpdatedAt) if err != nil { t.Fatal(err) } job := jobs.Job{ID: fixture.Job.ID, Status: jobs.Status(fixture.Job.Status), Capability: fixture.Job.Capability, OutputAssetIDs: fixture.Job.OutputAssetIDs, Error: fixture.Job.Error, UpdatedAt: updatedAt} body, err := Body(job) if err != nil { t.Fatal(err) } if string(body) != fixture.Body { t.Fatalf("body = %s, want %s", body, fixture.Body) } if got := Sign(body, fixture.Secret); got != fixture.Signature { t.Fatalf("signature = %q, want %q", got, fixture.Signature) } } func TestDeliverRetriesUntilSuccessAndPreservesExactRequest(t *testing.T) { clock := time.Date(2026, 8, 13, 8, 0, 0, 0, time.UTC) sender := &senderStub{responses: []sendResult{{response: Response{Status: 503}}, {err: errors.New("network")}, {response: Response{Status: 204}}}} deliverer := NewDeliverer(sender, " secret ", func() time.Time { return clock }) job := jobs.Job{ID: "job", Status: jobs.StatusSucceeded, Capability: "image.generate", OutputAssetIDs: []string{}, UpdatedAt: clock, WebhookURL: "https://hooks.example.test/job"} result, err := deliverer.Deliver(context.Background(), job) if err != nil || result.Attempts != 3 || result.LastStatus == nil || !result.LastStatus.OK || result.LastStatus.Status != 204 { t.Fatalf("Deliver = %#v, %v", result, err) } if len(sender.requests) != 3 || sender.requests[0].Headers["Content-Type"] != "application/json" || sender.requests[0].Headers["User-Agent"] != UserAgent || sender.requests[0].Headers["X-Zhinian-Signature"] == "" { t.Fatalf("requests = %#v", sender.requests) } } func TestDeliverRespectsPersistedAttemptCeiling(t *testing.T) { sender := &senderStub{} deliverer := NewDeliverer(sender, "", nil) result, err := deliverer.Deliver(context.Background(), jobs.Job{WebhookURL: "https://hooks.example.test", WebhookAttempts: 3}) if err != nil || result.Attempts != 3 || len(sender.requests) != 0 { t.Fatalf("Deliver = %#v, %v requests=%d", result, err, len(sender.requests)) } } type sendResult struct { response Response err error } type senderStub struct { responses []sendResult requests []Request } func (stub *senderStub) Send(_ context.Context, request Request) (Response, error) { stub.requests = append(stub.requests, request) result := stub.responses[len(stub.requests)-1] return result.response, result.err } type webhookFixture struct { Secret string `json:"secret"` Body string `json:"body"` Signature string `json:"signature"` Job struct { ID string `json:"id"` Status string `json:"status"` Capability string `json:"capability"` OutputAssetIDs []string `json:"outputAssetIds"` Error *jobs.JobError `json:"error"` UpdatedAt string `json:"updatedAt"` } `json:"job"` } func loadWebhookFixture(t *testing.T) webhookFixture { t.Helper() _, filename, _, ok := runtime.Caller(0) if !ok { t.Fatal("locate fixture") } raw, err := os.ReadFile(filepath.Join(filepath.Dir(filename), "..", "..", "..", "contracts", "webhook", "job-webhook-v1.json")) if err != nil { t.Fatal(err) } var fixture webhookFixture if err := json.Unmarshal(raw, &fixture); err != nil { t.Fatal(err) } return fixture }