1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import * as eventbridge from "@distilled.cloud/aws/eventbridge";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Redacted from "effect/Redacted";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11const CONNECTION_NAME = "alchemy-test-eb-connection";
12const DESTINATION_NAME = "alchemy-test-eb-destination";
13
14test.provider(
15 "connection (Redacted API key) + api destination lifecycle",
16 (stack) =>
17 Effect.gen(function* () {
18 yield* stack.destroy();
19
20 // Create — the API key value is Redacted end-to-end; EventBridge
21 // stores it in Secrets Manager.
22 const deployOnce = (options: {
23 description?: string;
24 rateLimit?: number;
25 }) =>
26 stack.deploy(
27 Effect.gen(function* () {
28 const connection = yield* AWS.EventBridge.Connection(
29 "TestConnection",
30 {
31 name: CONNECTION_NAME,
32 description: options.description,
33 authorizationType: "API_KEY",
34 authParameters: {
35 apiKeyAuthParameters: {
36 apiKeyName: "x-api-key",
37 apiKeyValue: Redacted.make("alchemy-test-secret-value"),
38 },
39 },
40 },
41 );
42 const destination = yield* AWS.EventBridge.ApiDestination(
43 "TestDestination",
44 {
45 name: DESTINATION_NAME,
46 connectionArn: connection.connectionArn,
47 invocationEndpoint: "https://example.com/events",
48 httpMethod: "POST",
49 invocationRateLimitPerSecond: options.rateLimit,
50 },
51 );
52 return { connection, destination };
53 }),
54 );
55
56 const { connection, destination } = yield* deployOnce({ rateLimit: 5 });
57
58 expect(connection.connectionName).toBe(CONNECTION_NAME);
59 expect(connection.connectionState).toBe("AUTHORIZED");
60 expect(connection.secretArn).toBeTruthy();
61 expect(destination.apiDestinationName).toBe(DESTINATION_NAME);
62 expect(destination.apiDestinationState).toBe("ACTIVE");
63
64 // Out-of-band verify via distilled — the secret never appears in
65 // attributes or describe output (only its Secrets Manager ARN).
66 const describedConnection = yield* eventbridge.describeConnection({
67 Name: CONNECTION_NAME,
68 });
69 expect(describedConnection.AuthorizationType).toBe("API_KEY");
70 expect(describedConnection.SecretArn).toBeTruthy();
71 expect(
72 describedConnection.AuthParameters?.ApiKeyAuthParameters?.ApiKeyName,
73 ).toBe("x-api-key");
74
75 const describedDestination = yield* eventbridge.describeApiDestination({
76 Name: DESTINATION_NAME,
77 });
78 expect(describedDestination.InvocationEndpoint).toBe(
79 "https://example.com/events",
80 );
81 expect(describedDestination.InvocationRateLimitPerSecond).toBe(5);
82
83 // Update — description and rate limit sync in place (no replace).
84 const { connection: updatedConnection, destination: updatedDestination } =
85 yield* deployOnce({ description: "updated connection", rateLimit: 10 });
86 expect(updatedConnection.connectionArn).toBe(connection.connectionArn);
87 expect(updatedDestination.apiDestinationArn).toBe(
88 destination.apiDestinationArn,
89 );
90
91 const afterUpdate = yield* eventbridge.describeConnection({
92 Name: CONNECTION_NAME,
93 });
94 expect(afterUpdate.Description).toBe("updated connection");
95 const destinationAfterUpdate = yield* eventbridge.describeApiDestination({
96 Name: DESTINATION_NAME,
97 });
98 expect(destinationAfterUpdate.InvocationRateLimitPerSecond).toBe(10);
99
100 yield* stack.destroy();
101
102 // Typed wait-until-gone — connection deletion is asynchronous
103 // (DELETING state), so poll until NotFound.
104 const gone = yield* eventbridge
105 .describeConnection({ Name: CONNECTION_NAME })
106 .pipe(
107 Effect.map(() => false),
108 Effect.catchTag("ResourceNotFoundException", () =>
109 Effect.succeed(true),
110 ),
111 Effect.repeat({
112 schedule: Schedule.spaced("3 seconds"),
113 until: (isGone): boolean => isGone,
114 times: 15,
115 }),
116 );
117 expect(gone).toBe(true);
118
119 const destinationGone = yield* eventbridge
120 .describeApiDestination({ Name: DESTINATION_NAME })
121 .pipe(
122 Effect.map(() => false),
123 Effect.catchTag("ResourceNotFoundException", () =>
124 Effect.succeed(true),
125 ),
126 );
127 expect(destinationGone).toBe(true);
128 }),
129 {
130 tags: ["provider:aws", "provider:aws:eventbridge", "live"],
131 timeout: 180_000,
132 },
133);