1import * as AWS from "@/AWS";
2import { Role } from "@/AWS/IAM/Role.ts";
3import { Stream as KinesisStream } from "@/AWS/Kinesis/Stream.ts";
4import { Destination } from "@/AWS/Logs/Destination.ts";
5import * as Test from "@/Test/Alchemy";
6import * as logs from "@distilled.cloud/aws/cloudwatch-logs";
7import { expect } from "alchemy-test";
8import * as Data from "effect/Data";
9import * as Effect from "effect/Effect";
10import * as Schedule from "effect/Schedule";
11
12const { test } = Test.make({ providers: AWS.providers() });
13
14const findDestination = Effect.fn(function* (destinationName: string) {
15 const described = yield* logs.describeDestinations({
16 DestinationNamePrefix: destinationName,
17 });
18 return (described.destinations ?? []).find(
19 (destination) => destination.destinationName === destinationName,
20 );
21});
22
23class DestinationStillExists extends Data.TaggedError(
24 "DestinationStillExists",
25)<{ readonly destinationName: string }> {}
26
27const assertDestinationDeleted = (destinationName: string) =>
28 findDestination(destinationName).pipe(
29 Effect.flatMap((destination) =>
30 destination === undefined
31 ? Effect.void
32 : Effect.fail(new DestinationStillExists({ destinationName })),
33 ),
34 Effect.retry({
35 while: (e) => e._tag === "DestinationStillExists",
36 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
37 }),
38 );
39
40// Cross-account subscriptions need a second account to exercise end-to-end;
41// this suite covers the single-account lifecycle (create/read/update/delete)
42// per the catalog's scope note.
43const infra = (allowedAccount: string) =>
44 Effect.gen(function* () {
45 const stream = yield* KinesisStream("DestinationStream", {});
46 const role = yield* Role("DestinationDeliveryRole", {
47 assumeRolePolicyDocument: {
48 Version: "2012-10-17",
49 Statement: [
50 {
51 Effect: "Allow",
52 Principal: { Service: "logs.amazonaws.com" },
53 Action: ["sts:AssumeRole"],
54 },
55 ],
56 },
57 inlinePolicies: {
58 "kinesis-put": {
59 Version: "2012-10-17",
60 Statement: [
61 {
62 Effect: "Allow",
63 Action: ["kinesis:PutRecord"],
64 Resource: [stream.streamArn],
65 },
66 ],
67 },
68 },
69 });
70 return yield* Destination("CentralLogs", {
71 destinationName: "alchemy-test-logs-destination",
72 targetArn: stream.streamArn,
73 roleArn: role.roleArn,
74 accessPolicy: {
75 Version: "2012-10-17",
76 Statement: [
77 {
78 Sid: "AllowSubscribers",
79 Effect: "Allow",
80 Principal: { AWS: allowedAccount },
81 Action: ["logs:PutSubscriptionFilter"],
82 Resource: ["*"],
83 },
84 ],
85 },
86 });
87 });
88
89test.provider(
90 "create, update access policy, delete destination",
91 (stack) =>
92 Effect.gen(function* () {
93 yield* stack.destroy();
94
95 const created = yield* stack.deploy(infra("391965393224"));
96
97 expect(created.destinationName).toBe("alchemy-test-logs-destination");
98 expect(created.destinationArn).toContain(":destination:");
99
100 // out-of-band verification via distilled
101 const observedCreated = yield* findDestination(created.destinationName);
102 expect(observedCreated?.targetArn).toBe(created.targetArn);
103 expect(observedCreated?.roleArn).toBe(created.roleArn);
104 expect(observedCreated?.accessPolicy).toContain("AllowSubscribers");
105
106 yield* stack.destroy();
107 yield* assertDestinationDeleted(created.destinationName);
108 }).pipe(Effect.onError(() => stack.destroy().pipe(Effect.ignore))),
109 {
110 tags: [
111 "provider:aws",
112 "provider:aws:iam",
113 "provider:aws:kinesis",
114 "provider:aws:logs",
115 "live",
116 ],
117 timeout: 240_000,
118 },
119);