Files
orbit/sdk/go/recovery_test.go

106 lines
3.8 KiB
Go

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")
}
}