1import * as AWS from "@/AWS";
2import { Subscription } from "@/AWS/SNS";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as SNS from "@distilled.cloud/aws/sns";
6import { expect } from "alchemy-test";
7import * as Data from "effect/Data";
8import * as Effect from "effect/Effect";
9import * as Schedule from "effect/Schedule";
10
11import {
12 SubscriptionTargetFunction,
13 SubscriptionTargetFunctionLive,
14} from "./subscription-handler.ts";
15
16const { test } = Test.make({ providers: AWS.providers() });
17
18test.provider(
19 "create and delete lambda subscription",
20 (stack) =>
21 Effect.gen(function* () {
22 yield* stack.destroy();
23
24 const deployed = yield* stack.deploy(
25 Effect.gen(function* () {
26 const topic = yield* AWS.SNS.Topic("LambdaSubscriptionTopic");
27 const target = yield* SubscriptionTargetFunction;
28 const subscription = yield* Subscription("LambdaSubscription", {
29 topicArn: topic.topicArn,
30 protocol: "lambda",
31 endpoint: target.functionArn,
32 returnSubscriptionArn: true,
33 });
34
35 return {
36 topic,
37 subscription,
38 };
39 }).pipe(Effect.provide(SubscriptionTargetFunctionLive)),
40 );
41
42 expect(deployed.subscription.subscriptionArn).toBeDefined();
43
44 const attributes = yield* SNS.getSubscriptionAttributes({
45 SubscriptionArn: deployed.subscription.subscriptionArn,
46 }).pipe(
47 Effect.tapError((err) =>
48 Effect.logError(deployed.subscription.subscriptionArn, err),
49 ),
50 Effect.retry({
51 while: (error) => error._tag === "NotFoundException",
52 schedule: Schedule.fixed(300),
53 times: 10,
54 }),
55 );
56 expect(attributes.Attributes?.Protocol).toBe("lambda");
57 expect(attributes.Attributes?.TopicArn).toBe(deployed.topic.topicArn);
58
59 yield* stack.destroy();
60 yield* assertSubscriptionDeleted(deployed.subscription.subscriptionArn);
61 }),
62 // ~60s in isolation (Lambda bundling + role propagation + SNS eventual
63 // consistency); budget headroom for full-suite contention.
64 {
65 tags: ["provider:aws", "provider:aws:lambda", "provider:aws:sns", "live"],
66 timeout: 180_000,
67 },
68);
69
70// Canonical `list()` test (AWS account/region-scoped collection): deploy a
71// topic + queue + SQS subscription, resolve the provider from context via the
72// typed `findProvider`, call `list()`, and assert the deployed subscription
73// appears in the exhaustively-paginated result.
74test.provider(
75 "list enumerates the deployed subscription",
76 (stack) =>
77 Effect.gen(function* () {
78 yield* stack.destroy();
79
80 const deployed = yield* stack.deploy(
81 Effect.gen(function* () {
82 const topic = yield* AWS.SNS.Topic("ListTopic");
83 const queue = yield* AWS.SQS.Queue("ListQueue");
84 const subscription = yield* Subscription("ListSubscription", {
85 topicArn: topic.topicArn,
86 protocol: "sqs",
87 endpoint: queue.queueArn,
88 returnSubscriptionArn: true,
89 });
90 return { subscription };
91 }),
92 );
93
94 expect(deployed.subscription.subscriptionArn).toBeDefined();
95
96 const provider = yield* Provider.findProvider(Subscription);
97 const all = yield* provider.list();
98
99 expect(
100 all.some(
101 (s) => s.subscriptionArn === deployed.subscription.subscriptionArn,
102 ),
103 ).toBe(true);
104
105 yield* stack.destroy();
106 yield* assertSubscriptionDeleted(deployed.subscription.subscriptionArn);
107 }),
108 {
109 tags: ["provider:aws", "provider:aws:sns", "provider:aws:sqs", "live"],
110 timeout: 180_000,
111 },
112);
113
114class SubscriptionStillExists extends Data.TaggedError(
115 "SubscriptionStillExists",
116) {}
117
118const assertSubscriptionDeleted = Effect.fn(function* (
119 subscriptionArn: string,
120) {
121 yield* SNS.getSubscriptionAttributes({
122 SubscriptionArn: subscriptionArn,
123 }).pipe(
124 Effect.flatMap(() => Effect.fail(new SubscriptionStillExists())),
125 Effect.retry({
126 while: (error) => error._tag === "SubscriptionStillExists",
127 schedule: Schedule.exponential(100),
128 times: 8,
129 }),
130 Effect.catchTag("NotFoundException", () => Effect.void),
131 Effect.catchTag("InvalidParameterException", () => Effect.void),
132 );
133});