package phasea import ( "context" "encoding/json" "errors" "fmt" "os" "strings" "sync" "sync/atomic" "testing" "time" ) func TestValidationRejectsInvalidInputsBeforePersistence(t *testing.T) { store := &Store{} if err := store.CreateAccount(context.Background(), Account{ID: "INVALID"}); !errors.Is(err, ErrInvalid) { t.Fatalf("expected invalid account, got %v", err) } if err := store.CreateAccount(context.Background(), Account{ ID: "account-a", CredentialReferenceID: "credential-a", CredentialProvider: "os_keyring", CredentialKey: "raw-value", ProfileID: "profile-a", }); !errors.Is(err, ErrInvalid) { t.Fatalf("expected non-reference credential value to be rejected, got %v", err) } if _, _, err := store.Enqueue(context.Background(), Task{ID: "task-a"}); !errors.Is(err, ErrInvalid) { t.Fatalf("expected invalid task, got %v", err) } if _, err := store.ExecuteMock(context.Background(), "worker-a", "retry"); !errors.Is(err, ErrInvalid) { t.Fatalf("expected unsupported outcome to be rejected, got %v", err) } } func TestPhaseAOfflineWorkflow(t *testing.T) { databaseURL := os.Getenv("CREATORHUB_POSTGRES_TEST_URL") if databaseURL == "" { t.Skip("set CREATORHUB_POSTGRES_TEST_URL to run PostgreSQL integration coverage") } ctx := context.Background() store, err := Open(ctx, databaseURL) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = store.Close() }) if _, err := store.db.ExecContext(ctx, ` TRUNCATE audit_event, execution_attempt, operation_task, confirmation, content_draft, runtime_instance, social_account, credential_reference RESTART IDENTITY CASCADE`); err != nil { t.Fatal(err) } accounts := []Account{ {ID: "account-a", CredentialReferenceID: "credential-a", CredentialProvider: "os_keyring", CredentialKey: "creatorhub/account-a", ProfileID: "profile-a"}, {ID: "account-b", CredentialReferenceID: "credential-b", CredentialProvider: "os_keyring", CredentialKey: "creatorhub/account-b", ProfileID: "profile-b"}, } for _, account := range accounts { if err := store.CreateAccount(ctx, account); err != nil { t.Fatal(err) } } if err := store.CreateAccount(ctx, Account{ ID: "account-c", CredentialReferenceID: "credential-c", CredentialProvider: "os_keyring", CredentialKey: "creatorhub/account-c", ProfileID: "profile-a", }); !errors.Is(err, ErrConflict) { t.Fatalf("expected profiles to be unique, got %v", err) } lease := time.Now().Add(time.Minute) if err := store.BindRuntime(ctx, RuntimeBinding{ID: "binding-a", AccountID: "account-a", RuntimeID: "runtime-a", LeaseUntil: lease}); err != nil { t.Fatal(err) } if err := store.BindRuntime(ctx, RuntimeBinding{ID: "binding-a-2", AccountID: "account-a", RuntimeID: "runtime-b", LeaseUntil: lease}); !errors.Is(err, ErrConflict) { t.Fatalf("expected one active runtime per account, got %v", err) } if err := store.BindRuntime(ctx, RuntimeBinding{ID: "binding-b", AccountID: "account-b", RuntimeID: "runtime-a", LeaseUntil: lease}); !errors.Is(err, ErrConflict) { t.Fatalf("expected one account per runtime, got %v", err) } if err := store.ReleaseRuntime(ctx, "binding-a"); err != nil { t.Fatal(err) } createApprovedDraft(t, store, "account-a", "draft-a", "confirmation-a") for index := range 20 { task := approvedTask(index, "account-a", "draft-a", "confirmation-a") if _, inserted, err := store.Enqueue(ctx, task); err != nil || !inserted { t.Fatalf("enqueue task %d: inserted=%v err=%v", index, inserted, err) } task.ID = fmt.Sprintf("duplicate-%02d", index) if existing, inserted, err := store.Enqueue(ctx, task); err != nil || inserted || existing.ID == task.ID { t.Fatalf("redeliver task %d: existing=%q inserted=%v err=%v", index, existing.ID, inserted, err) } } var executed atomic.Int64 var workers sync.WaitGroup errorsFromWorkers := make(chan error, 4) for worker := range 4 { workers.Add(1) go func() { defer workers.Done() for { execution, err := store.ExecuteMock(ctx, fmt.Sprintf("worker-%d", worker), "succeeded") if err != nil { errorsFromWorkers <- err return } if !execution.WasClaimed { return } executed.Add(1) } }() } workers.Wait() close(errorsFromWorkers) for err := range errorsFromWorkers { t.Fatal(err) } if executed.Load() != 20 { t.Fatalf("expected exactly 20 executions, got %d", executed.Load()) } assertCount(t, store, `SELECT count(*) FROM operation_task WHERE state = 'succeeded'`, 20) assertCount(t, store, `SELECT count(*) FROM execution_attempt`, 20) createApprovedDraft(t, store, "account-b", "draft-b", "confirmation-b") unconfirmed := approvedTask(30, "account-b", "draft-b", "") unconfirmed.ConfirmationVersion = 0 if _, _, err := store.Enqueue(ctx, unconfirmed); err != nil { t.Fatal(err) } mismatch := approvedTask(31, "account-b", "draft-b", "confirmation-b") mismatch.AccountVersion = 2 if _, _, err := store.Enqueue(ctx, mismatch); err != nil { t.Fatal(err) } if execution, err := store.ExecuteMock(ctx, "worker-validation", "succeeded"); err != nil || execution.WasClaimed { t.Fatalf("invalid tasks must not execute: execution=%+v err=%v", execution, err) } assertCount(t, store, `SELECT count(*) FROM operation_task WHERE id IN ('task-30', 'task-31') AND state = 'needs_confirmation'`, 2) assertCount(t, store, `SELECT count(*) FROM execution_attempt WHERE task_id IN ('task-30', 'task-31')`, 0) uncertain := approvedTask(32, "account-b", "draft-b", "confirmation-b") if _, _, err := store.Enqueue(ctx, uncertain); err != nil { t.Fatal(err) } if execution, err := store.ExecuteMock(ctx, "worker-uncertain", "uncertain"); err != nil || execution.State != "needs_confirmation" { t.Fatalf("uncertain outcome was not held: execution=%+v err=%v", execution, err) } if execution, err := store.ExecuteMock(ctx, "worker-uncertain", "succeeded"); err != nil || execution.WasClaimed { t.Fatalf("uncertain task was retried: execution=%+v err=%v", execution, err) } assertCount(t, store, `SELECT count(*) FROM execution_attempt WHERE task_id = 'task-32'`, 1) policy := approvedTask(33, "account-b", "draft-b", "confirmation-b") if _, _, err := store.Enqueue(ctx, policy); err != nil { t.Fatal(err) } if execution, err := store.ExecuteMock(ctx, "worker-policy", "policy_hold"); err != nil || execution.State != "policy_hold" { t.Fatalf("policy outcome was not held: execution=%+v err=%v", execution, err) } if execution, err := store.ExecuteMock(ctx, "worker-policy", "succeeded"); err != nil || execution.WasClaimed { t.Fatalf("policy task was retried: execution=%+v err=%v", execution, err) } paused := approvedTask(34, "account-b", "draft-b", "confirmation-b") if _, _, err := store.Enqueue(ctx, paused); err != nil { t.Fatal(err) } pauseStarted := time.Now() if err := store.PauseAccount(ctx, "account-b"); err != nil { t.Fatal(err) } if err := store.PauseAccount(ctx, "account-b"); err != nil { t.Fatalf("pause should be idempotent: %v", err) } if execution, err := store.ExecuteMock(ctx, "worker-paused", "succeeded"); err != nil || execution.WasClaimed { t.Fatalf("paused account task was claimed: execution=%+v err=%v", execution, err) } assertCount(t, store, `SELECT count(*) FROM operation_task WHERE id = 'task-34' AND state = 'policy_hold'`, 1) var pausedAt time.Time if err := store.db.QueryRowContext(ctx, `SELECT paused_at FROM social_account WHERE id = 'account-b'`).Scan(&pausedAt); err != nil || pausedAt.Sub(pauseStarted) >= time.Minute { t.Fatalf("account was not paused within one minute: paused_at=%v err=%v", pausedAt, err) } cancelled := approvedTask(35, "account-a", "draft-a", "confirmation-a") if _, _, err := store.Enqueue(ctx, cancelled); err != nil { t.Fatal(err) } if err := store.CancelTask(ctx, cancelled.ID); err != nil { t.Fatal(err) } assertCount(t, store, `SELECT count(*) FROM operation_task WHERE id = 'task-35' AND state = 'cancelled'`, 1) expired := approvedTask(36, "account-a", "draft-a", "confirmation-a") if _, _, err := store.Enqueue(ctx, expired); err != nil { t.Fatal(err) } claimed, err := store.claim(ctx, "worker-crashed") if err != nil || !claimed.WasClaimed { t.Fatalf("claim task for lease test: execution=%+v err=%v", claimed, err) } if _, err := store.db.ExecContext(ctx, `UPDATE operation_task SET lease_until = now() - interval '1 second' WHERE id = $1`, claimed.TaskID); err != nil { t.Fatal(err) } if execution, err := store.ExecuteMock(ctx, "worker-after-crash", "succeeded"); err != nil || execution.WasClaimed { t.Fatalf("expired unknown result was retried: execution=%+v err=%v", execution, err) } assertCount(t, store, `SELECT count(*) FROM operation_task WHERE id = 'task-36' AND state = 'needs_confirmation'`, 1) assertCount(t, store, `SELECT count(*) FROM execution_attempt WHERE task_id = 'task-36' AND outcome = 'uncertain'`, 1) events, err := store.Audit(ctx) if err != nil { t.Fatal(err) } traceable := false for _, event := range events { if event.EventType == "task_finished" && event.ConfirmationID != "" && event.ConfirmationVersion == 1 && event.AttemptID != "" && event.TaskID != "" { traceable = true break } } if !traceable { t.Fatal("audit does not trace confirmation version, task, and attempt") } exported, _ := json.Marshal(events) for _, forbidden := range []string{"password", "cookie", "token", "credential-a", "creatorhub/account-a"} { if strings.Contains(strings.ToLower(string(exported)), forbidden) { t.Fatalf("audit export contains sensitive field or credential reference %q", forbidden) } } if _, err := store.db.ExecContext(ctx, `UPDATE audit_event SET event_type = 'rewritten' WHERE id = 1`); err == nil { t.Fatal("audit events must be append-only") } } func createApprovedDraft(t *testing.T, store *Store, accountID, draftID, confirmationID string) { t.Helper() ctx := context.Background() if err := store.CreateDraft(ctx, Draft{ID: draftID, AccountID: accountID, Version: 1, Content: "offline mock content"}); err != nil { t.Fatal(err) } if err := store.Confirm(ctx, Confirmation{ ID: confirmationID, AccountID: accountID, AccountVersion: 1, DraftID: draftID, DraftVersion: 1, Version: 1, }); err != nil { t.Fatal(err) } } func approvedTask(index int, accountID, draftID, confirmationID string) Task { return Task{ ID: fmt.Sprintf("task-%02d", index), IdempotencyKey: fmt.Sprintf("idempotency-%02d", index), AccountID: accountID, AccountVersion: 1, DraftID: draftID, DraftVersion: 1, ConfirmationID: confirmationID, ConfirmationVersion: 1, } } func assertCount(t *testing.T, store *Store, query string, expected int) { t.Helper() var actual int if err := store.db.QueryRowContext(context.Background(), query).Scan(&actual); err != nil || actual != expected { t.Fatalf("count mismatch: expected=%d actual=%d err=%v query=%s", expected, actual, err, query) } }