1import * as AWS from "@/AWS";
2import type { PolicyDocument } from "@/AWS/IAM/Policy.ts";
3import { Role } from "@/AWS/IAM/Role.ts";
4import { Destination } from "@/AWS/IoTManagedIntegrations";
5import { Stream } from "@/AWS/Kinesis";
6import { Region } from "@/AWS/Region.ts";
7import * as Test from "@/Test/Alchemy";
8import * as mi from "@distilled.cloud/aws/iot-managed-integrations";
9import { expect } from "alchemy-test";
10import * as Data from "effect/Data";
11import * as Effect from "effect/Effect";
12import * as Schedule from "effect/Schedule";
13
14const { test } = Test.make({ providers: AWS.providers() });
15
16// IoT Managed Integrations is only deployed in a few regions (eu-west-1,
17// ca-central-1, ...). The testing profile's default region (us-west-2) has no
18// endpoint, so the ungated probe pins a supported region explicitly. The gated
19// lifecycle below requires an AWS profile whose region IS a supported one
20// (SSO profiles resolve region from ~/.aws/config; the AWS_REGION env var is
21// not consulted), e.g. a profile with region = eu-west-1.
22const MI_REGION = "eu-west-1";
23const pin = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
24 effect.pipe(Effect.provideService(Region, Effect.succeed(MI_REGION)));
25
26// Ungated typed-error probe: prove the distilled error union carries the
27// not-found tag this provider's read/delete paths depend on.
28test.provider(
29 "getDestination on a nonexistent destination fails with ResourceNotFoundException",
30 () =>
31 Effect.gen(function* () {
32 const error = yield* Effect.flip(
33 pin(mi.getDestination({ Name: "alchemy-nonexistent-destination" })),
34 );
35 expect(error._tag).toBe("ResourceNotFoundException");
36 }),
37 { tags: ["provider:aws", "provider:aws:iotmanagedintegrations", "live"] },
38);
39
40class DestinationStillExists extends Data.TaggedError(
41 "DestinationStillExists",
42)<{ readonly name: string }> {}
43
44const assertDestinationGone = (name: string) =>
45 mi.getDestination({ Name: name }).pipe(
46 Effect.flatMap(() => Effect.fail(new DestinationStillExists({ name }))),
47 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
48 Effect.retry({
49 while: (e) => e._tag === "DestinationStillExists",
50 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
51 }),
52 );
53
54// Full lifecycle is gated: the service only exists in a few regions. Run with
55// AWS_TEST_IOT_MI=1 and an AWS profile configured in a supported region.
56test.provider.skipIf(!process.env.AWS_TEST_IOT_MI)(
57 "create kinesis destination, verify, update description, destroy",
58 (stack) =>
59 Effect.gen(function* () {
60 yield* stack.destroy();
61
62 const makeStack = (description: string) =>
63 Effect.gen(function* () {
64 const stream = yield* Stream("Events", {});
65 const assumeRolePolicyDocument: PolicyDocument = {
66 Version: "2012-10-17",
67 Statement: [
68 {
69 Effect: "Allow",
70 Principal: {
71 Service: "iotmanagedintegrations.amazonaws.com",
72 },
73 Action: ["sts:AssumeRole"],
74 },
75 ],
76 };
77 const role = yield* Role("DeliveryRole", {
78 assumeRolePolicyDocument,
79 inlinePolicies: {
80 kinesis: {
81 Version: "2012-10-17",
82 Statement: [
83 {
84 Effect: "Allow",
85 Action: ["kinesis:PutRecord", "kinesis:PutRecords"],
86 Resource: [stream.streamArn],
87 },
88 ],
89 },
90 },
91 });
92 const destination = yield* Destination("EventDestination", {
93 deliveryDestinationArn: stream.streamArn,
94 roleArn: role.roleArn,
95 description,
96 tags: { fixture: "iot-mi-destination" },
97 });
98 return { destination };
99 });
100
101 const { destination } = yield* stack.deploy(makeStack("phase one"));
102
103 expect(destination.destinationName).toBeDefined();
104 expect(destination.deliveryDestinationType).toBe("KINESIS");
105 expect(destination.description).toBe("phase one");
106
107 // Out-of-band verification via distilled.
108 const observed = yield* mi.getDestination({
109 Name: destination.destinationName,
110 });
111 expect(observed.DeliveryDestinationArn).toBe(
112 destination.deliveryDestinationArn,
113 );
114 expect(observed.Tags?.fixture).toBe("iot-mi-destination");
115
116 // Update the description in place.
117 const { destination: updated } = yield* stack.deploy(
118 makeStack("phase two"),
119 );
120 expect(updated.destinationName).toBe(destination.destinationName);
121 expect(updated.description).toBe("phase two");
122
123 yield* stack.destroy();
124 yield* assertDestinationGone(destination.destinationName);
125 }),
126 {
127 tags: [
128 "provider:aws",
129 "provider:aws:iam",
130 "provider:aws:iotmanagedintegrations",
131 "provider:aws:kinesis",
132 "live",
133 ],
134 timeout: 240_000,
135 },
136);