package contract_test import ( "encoding/json" "errors" "reflect" "strings" "testing" "time" "git.5iot.com/iotlcm-public/iotlcm-worker-svr/internal/fullsync/contract" ) func TestReplayRequestIdentity(t *testing.T) { request := contract.ReplayRequest{SourceRunID: "source", IdempotencyKey: "key", RequestedBy: "operator"} fingerprint, err := request.Fingerprint() if err != nil || len(fingerprint) != 64 { t.Fatalf("fingerprint=%q, err=%v", fingerprint, err) } for name, mutate := range map[string]func(*contract.ReplayRequest){ "same": func(*contract.ReplayRequest) {}, "key": func(r *contract.ReplayRequest) { r.IdempotencyKey = "another-key" }, "source": func(r *contract.ReplayRequest) { r.SourceRunID = "another-source" }, "body": func(r *contract.ReplayRequest) { r.RequestedBy = "another-operator" }, } { copy := request mutate(©) got, err := copy.Fingerprint() if err != nil || (got == fingerprint) != (name == "same" || name == "key") { t.Errorf("%s fingerprint=%s err=%v", name, got, err) } } if got := string(mustJSON(t, request)); got != `{"requestedBy":"operator"}` { t.Fatalf("path/header leaked into body: %s", got) } for _, bad := range []contract.ReplayRequest{ {SourceRunID: "source", RequestedBy: "operator"}, {SourceRunID: "source", IdempotencyKey: "key", RequestedBy: " "}, {SourceRunID: "{source}", IdempotencyKey: "key", RequestedBy: "operator"}, {SourceRunID: "source", IdempotencyKey: "key\r\ninjected", RequestedBy: "operator"}, {SourceRunID: "source", IdempotencyKey: strings.Repeat("x", 257), RequestedBy: "operator"}, } { if _, err := bad.Fingerprint(); !errors.Is(err, contract.ErrInvalidArgument) { t.Errorf("bad request returned %v", err) } } } func TestActionProgressAndReceiptValidation(t *testing.T) { for _, progress := range []contract.ReplayProgress{ {DiscoveredCount: -1}, {DiscoveredCount: 1, EnqueuedCount: 2}, {DiscoveredCount: 2, EnqueuedCount: 1, CleanedCount: 2}, {FailureCount: -1}, } { if err := progress.Validate(); !errors.Is(err, contract.ErrInvalidArgument) { t.Fatalf("bad progress %+v returned %v", progress, err) } } now := time.Now().UTC() action := contract.ReplayAction{ActionID: "action", SourceRunID: "source", TargetRunID: "target", RequestedBy: "operator", ReplayCycle: 1, Status: contract.ActionAccepted} if err := action.Validate(); err != nil { t.Fatal(err) } action.Status = contract.ActionCompleted if err := action.Validate(); !errors.Is(err, contract.ErrInvalidArgument) { t.Fatalf("completed action without receipt returned %v", err) } action.Receipt = &contract.ArchiveReceipt{Version: 1, Path: "receipt.json", Checksum: strings.Repeat("a", 64), ArchivedAt: now} action.CompletedAt = &now if err := action.Validate(); err != nil { t.Fatal(err) } action.Receipt.Checksum = "not-a-checksum" if err := action.Validate(); !errors.Is(err, contract.ErrInvalidArgument) { t.Fatalf("bad receipt returned %v", err) } } func TestActionWireFormatHidesInternalState(t *testing.T) { action := contract.ReplayAction{ ActionID: "action", SourceRunID: "source", TargetRunID: "target", RequestedBy: "operator", ReplayCycle: 1, Status: contract.ActionCleanupPending, IdempotencyKey: "secret", Fingerprint: "internal-fingerprint", LeaseOwner: "worker", LeaseToken: "token", LeaseUntil: time.Now(), Revision: 9, Progress: contract.ReplayProgress{ScanCursor: "opaque", DiscoveredCount: 10, EnqueuedCount: 8, CleanedCount: 7, FailureCount: 2}, } data := mustJSON(t, action) for _, hidden := range []string{"idempotency", "fingerprint", "lease", "revision", "secret", "token"} { if strings.Contains(strings.ToLower(string(data)), hidden) { t.Errorf("internal %s leaked: %s", hidden, data) } } var decoded contract.ReplayAction if err := json.Unmarshal(data, &decoded); err != nil || !reflect.DeepEqual(decoded.Progress, action.Progress) || decoded.Status != contract.ActionCleanupPending { t.Fatalf("action roundtrip lost state: %+v, %v", decoded, err) } statusURL, err := contract.ActionStatusURL("action%1") if err != nil || statusURL != "/internal/v1/full-sync-actions/action%251" { t.Fatalf("status URL=%s, %v", statusURL, err) } accepted := contract.ReplayAccepted{ActionID: "action", TargetRunID: "target", Status: contract.ActionAccepted, StatusURL: "/internal/v1/full-sync-actions/action"} if got := string(mustJSON(t, accepted)); got != `{"actionId":"action","targetRunId":"target","status":"accepted","statusUrl":"/internal/v1/full-sync-actions/action"}` { t.Fatalf("202 contract changed: %s", got) } }