package orbit import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" ) func TestRecoveryReceiptAndAuditBoundary(t *testing.T) { const task = "11111111111111111111111111111111" const child = "22222222222222222222222222222222" const audit = "33333333333333333333333333333333" var calls atomic.Int32 var malformed atomic.Bool server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls.Add(1) if r.Header.Get("Authorization") != "Bearer example-token" { t.Error("missing token") } var data any switch r.URL.Path { case "/v1/tasks/" + task + ":review": if r.Method != "POST" || r.Header.Get("Idempotency-Key") != "stable-review" { t.Error("review identity") } var in ReviewTaskRequest if err := json.NewDecoder(r.Body).Decode(&in); err != nil || in.ExpectedRevision != 9007199254740993 || !in.ExecutionSettled { t.Error("lost review fields", err) } out := ReviewTaskResult{AuditID: audit, Task: Task{ID: task, State: TaskFailed, Revision: 9007199254740994}} if malformed.Load() { out.Task.EffectUncertain = true } data = out case "/v1/tasks/" + task + ":retry": if r.Method != "POST" || r.Header.Get("Idempotency-Key") != "stable-retry" { t.Error("retry identity") } parent := task out := RetryTaskResult{AuditID: audit, SourceTaskID: task, Task: Task{ID: child, RetryOf: &parent}} if malformed.Load() { out.Task.RetryOf = nil } data = out case "/v1/tasks/" + task + "/operations": if r.Method != "GET" || r.Header.Get("Idempotency-Key") != "" || r.URL.Query().Get("cursor") != "opaque+cursor/=" { t.Error("audit page request") } data = TaskOperationPage{Items: []TaskOperationSummary{{ID: audit, TaskID: task, Action: "task.review"}}} case "/v1/tasks/" + task + "/operations/" + audit: data = TaskOperation{TaskOperationSummary: TaskOperationSummary{ID: audit, TaskID: task}, After: json.RawMessage(`{"revision":"9007199254740994"}`)} default: t.Error("unexpected path", r.URL.Path) } json.NewEncoder(w).Encode(map[string]any{"request_id": audit, "data": data}) })) defer server.Close() c, _ := NewClient(server.URL, Options{Token: "example-token"}) ctx := context.Background() in := ReviewTaskRequest{ExpectedRevision: 9007199254740993, ExecutionSettled: true, Outcome: TaskFailed} review, err := c.ReviewTask(ctx, task, "stable-review", in) if err != nil || review.Task.Revision != 9007199254740994 { t.Fatal(review, err) } retry, err := c.RetryTask(ctx, task, "stable-retry", RetryTaskRequest{}) if err != nil || retry.Task.ID != child { t.Fatal(retry, err) } page, err := c.ListTaskOperations(ctx, task, HistoryPageRequest{Cursor: "opaque+cursor/="}) if err != nil || len(page.Items) != 1 { t.Fatal(page, err) } detail, err := c.GetTaskOperation(ctx, task, audit) if err != nil || string(detail.After) != `{"revision":"9007199254740994"}` { t.Fatal(detail, err) } malformed.Store(true) if _, err = c.ReviewTask(ctx, task, "stable-review", in); err == nil { t.Fatal("inconsistent review accepted") } if _, err = c.RetryTask(ctx, task, "stable-retry", RetryTaskRequest{}); err == nil { t.Fatal("missing retry parent accepted") } for _, id := range []string{"../tasks", strings.Repeat("A", 32), "id?token=secret"} { if _, err = c.ReviewTask(ctx, id, "key", in); err == nil { t.Fatal("unsafe path") } if _, err = c.RetryTask(ctx, id, "key", RetryTaskRequest{}); err == nil { t.Fatal("unsafe path") } if _, err = c.GetTaskOperation(ctx, task, id); err == nil { t.Fatal("unsafe audit path") } } if _, err = c.ListTaskOperations(ctx, task, HistoryPageRequest{Limit: 101}); err == nil { t.Fatal("unbounded page") } if calls.Load() != 6 { t.Fatal("invalid path reached transport") } }