1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import * as keyspaces from "@distilled.cloud/aws/keyspaces";
4import { describe, expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import * as HttpClient from "effect/http/HttpClient";
8
9import KeyspacesRestoreTestFunctionLive, {
10 KeyspacesRestoreTestFunction,
11} from "./restore-handler.ts";
12
13const { test } = Test.make({ providers: AWS.providers() });
14
15const RESTORE_KS = "alchemy_restore_test_ks";
16const RESTORED_TABLE = "orders_restored";
17
18// The restored table is created out-of-band by the Lambda (RestoreTable
19// provisions a brand-new table), so the test owns its cleanup. A table that
20// is still RESTORING rejects delete with ConflictException; retry bounded.
21const deleteRestoredTable = keyspaces
22 .deleteTable({ keyspaceName: RESTORE_KS, tableName: RESTORED_TABLE })
23 .pipe(
24 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
25 Effect.retry({
26 while: (e): boolean => e._tag === "ConflictException",
27 schedule: Schedule.max([
28 Schedule.fixed("5 seconds"),
29 Schedule.recurs(36),
30 ]),
31 }),
32 // Wait (bounded) until it is actually gone so the keyspace can be
33 // destroyed without a ConflictException.
34 Effect.andThen(
35 keyspaces
36 .getTable({ keyspaceName: RESTORE_KS, tableName: RESTORED_TABLE })
37 .pipe(
38 Effect.flatMap(() =>
39 Effect.fail(new Error(`'${RESTORED_TABLE}' still deleting`)),
40 ),
41 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
42 Effect.retry({
43 schedule: Schedule.max([
44 Schedule.fixed("3 seconds"),
45 Schedule.recurs(20),
46 ]),
47 }),
48 ),
49 ),
50 );
51
52// keyspaces RestoreTable creates a new table from a source table's
53// point-in-time-recovery backup. The ungated probe asserts the distilled
54// wiring surfaces typed errors; the live restore (which needs a PITR table +
55// a deployed Lambda exercising the RestoreTable binding) is gated behind
56// AWS_TEST_SLOW=1.
57describe(
58 "AWS.Keyspaces.RestoreTable",
59 { tags: ["provider:aws", "provider:aws:keyspaces", "live"] },
60 () => {
61 test.provider(
62 "restore of a nonexistent source table yields a typed error",
63 (_stack) =>
64 Effect.gen(function* () {
65 const error = yield* keyspaces
66 .restoreTable({
67 sourceKeyspaceName: "alchemy_nonexistent_ks",
68 sourceTableName: "nonexistent_tbl",
69 targetKeyspaceName: "alchemy_nonexistent_ks",
70 targetTableName: "restored_tbl",
71 })
72 .pipe(Effect.flip);
73 expect([
74 "ResourceNotFoundException",
75 "ValidationException",
76 ]).toContain(error._tag);
77 }),
78 { timeout: 60_000 },
79 );
80
81 test.provider.skipIf(!process.env.AWS_TEST_SLOW)(
82 "Lambda restores a PITR table into a new table via the binding",
83 (stack) =>
84 Effect.gen(function* () {
85 yield* stack.destroy();
86
87 const { functionUrl } = yield* stack.deploy(
88 Effect.gen(function* () {
89 return yield* KeyspacesRestoreTestFunction;
90 }).pipe(Effect.provide(KeyspacesRestoreTestFunctionLive)),
91 );
92 expect(functionUrl).toBeTruthy();
93 const baseUrl = functionUrl!.replace(/\/+$/, "");
94
95 // Pre-clean any restored table left over from a previous failed run
96 // — RestoreTable rejects when the target already exists.
97 yield* deleteRestoredTable;
98
99 // Drive the deployed Lambda through the RestoreTable binding —
100 // proves the cassandra:Restore/Select IAM actions and the client
101 // wiring. Retry through function-URL cold start / IAM propagation.
102 const body = yield* HttpClient.post(`${baseUrl}/restore`).pipe(
103 Effect.flatMap((response) =>
104 response.status === 200
105 ? response.json
106 : Effect.fail(
107 new Error(`restore not ready: ${response.status}`),
108 ),
109 ),
110 Effect.retry({
111 schedule: Schedule.max([
112 Schedule.exponential("1 second"),
113 Schedule.recurs(8),
114 ]),
115 }),
116 );
117 const restored = body as {
118 restoredTableARN?: string;
119 error?: string;
120 };
121 expect(restored.error).toBeUndefined();
122 expect(restored.restoredTableARN).toContain(
123 `/keyspace/${RESTORE_KS}/table/${RESTORED_TABLE}`,
124 );
125
126 // Out-of-band cleanup of the restored table, then tear down the
127 // stack-managed keyspace/table/function.
128 yield* deleteRestoredTable;
129 yield* stack.destroy();
130 }).pipe(Effect.ensuring(Effect.ignore(deleteRestoredTable))),
131 { tags: ["provider:aws:lambda"], timeout: 900_000 },
132 );
133 },
134);