1import * as AWS from "@/AWS";
2import { OptOutList } from "@/AWS/PinpointSMSVoiceV2";
3import * as Test from "@/Test/Alchemy";
4import * as smsvoice from "@distilled.cloud/aws/pinpoint-sms-voice-v2";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11// Ungated typed-error probe: proves the distilled error union carries the
12// not-found tag this provider's read/observe paths depend on.
13test.provider(
14 "describeOptOutLists on a nonexistent name fails with ResourceNotFoundException",
15 () =>
16 Effect.gen(function* () {
17 const error = yield* Effect.flip(
18 smsvoice.describeOptOutLists({
19 OptOutListNames: ["alchemy-nonexistent-opt-out-list-probe"],
20 }),
21 );
22 expect(error._tag).toBe("ResourceNotFoundException");
23 }),
24 {
25 tags: ["provider:aws", "provider:aws:pinpointsmsvoicev2", "live"],
26 timeout: 60_000,
27 },
28);
29
30const getOptOutList = (name: string) =>
31 smsvoice.describeOptOutLists({ OptOutListNames: [name] }).pipe(
32 Effect.map((r) => r.OptOutLists?.[0]),
33 Effect.catchTag("ResourceNotFoundException", () =>
34 Effect.succeed(undefined),
35 ),
36 );
37
38const assertOptOutListGone = (name: string) =>
39 Effect.gen(function* () {
40 const found = yield* getOptOutList(name);
41 if (found !== undefined) {
42 return yield* Effect.fail(
43 new Error(`opt-out list '${name}' still exists`),
44 );
45 }
46 }).pipe(
47 Effect.retry({
48 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(8)]),
49 }),
50 );
51
52// Opt-out lists are free and provision synchronously — the full lifecycle
53// (create, no-op, tag update, destroy) runs ungated.
54test.provider(
55 "create, update tags, and destroy an opt-out list",
56 (stack) =>
57 Effect.gen(function* () {
58 yield* stack.destroy();
59
60 const props = {
61 optOutListName: "alchemy-test-smsvoice-opt-out-list",
62 tags: { fixture: "smsvoice-opt-out-list" },
63 };
64
65 // Create.
66 const created = yield* stack.deploy(OptOutList("OptOuts", props));
67 expect(created.optOutListName).toBe("alchemy-test-smsvoice-opt-out-list");
68 expect(created.optOutListArn).toContain(":opt-out-list/");
69
70 // Out-of-band verification via distilled.
71 const observed = yield* getOptOutList(created.optOutListName);
72 expect(observed?.OptOutListName).toBe(created.optOutListName);
73 const tags = yield* smsvoice.listTagsForResource({
74 ResourceArn: created.optOutListArn,
75 });
76 const tagRecord = Object.fromEntries(
77 (tags.Tags ?? []).map((t) => [t.Key, t.Value]),
78 );
79 expect(tagRecord.fixture).toBe("smsvoice-opt-out-list");
80 expect(tagRecord["alchemy::id"]).toBe("OptOuts");
81
82 // No-op redeploy keeps the same list.
83 const noop = yield* stack.deploy(OptOutList("OptOuts", props));
84 expect(noop.optOutListArn).toBe(created.optOutListArn);
85
86 // Tag update in place — the ARN must not change.
87 const updated = yield* stack.deploy(
88 OptOutList("OptOuts", {
89 ...props,
90 tags: { fixture: "smsvoice-opt-out-list-v2", extra: "1" },
91 }),
92 );
93 expect(updated.optOutListArn).toBe(created.optOutListArn);
94 const retags = yield* smsvoice.listTagsForResource({
95 ResourceArn: created.optOutListArn,
96 });
97 const retagRecord = Object.fromEntries(
98 (retags.Tags ?? []).map((t) => [t.Key, t.Value]),
99 );
100 expect(retagRecord.fixture).toBe("smsvoice-opt-out-list-v2");
101 expect(retagRecord.extra).toBe("1");
102
103 // Destroy and verify out-of-band with a typed wait-until-gone.
104 yield* stack.destroy();
105 yield* assertOptOutListGone(created.optOutListName);
106 }),
107 {
108 tags: ["provider:aws", "provider:aws:pinpointsmsvoicev2", "live"],
109 timeout: 240_000,
110 },
111);