1import * as AWS from "@/AWS";
2import { CredentialLocker } from "@/AWS/IoTManagedIntegrations";
3import { Region } from "@/AWS/Region.ts";
4import * as Test from "@/Test/Alchemy";
5import * as mi from "@distilled.cloud/aws/iot-managed-integrations";
6import { expect } from "alchemy-test";
7import * as Data from "effect/Data";
8import * as Effect from "effect/Effect";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13// IoT Managed Integrations is only deployed in a few regions (eu-west-1,
14// ca-central-1, ...). The testing profile's default region (us-west-2) has no
15// endpoint, so the ungated probe pins a supported region explicitly. The gated
16// lifecycle below requires an AWS profile whose region IS a supported one
17// (SSO profiles resolve region from ~/.aws/config; the AWS_REGION env var is
18// not consulted), e.g. a profile with region = eu-west-1.
19const MI_REGION = "eu-west-1";
20const pin = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
21 effect.pipe(Effect.provideService(Region, Effect.succeed(MI_REGION)));
22
23// Ungated typed-error probe: prove the distilled error union carries the
24// not-found tag this provider's read/delete paths depend on. Runs in every
25// CI pass at near-zero cost, unlike the gated lifecycle below.
26test.provider(
27 "getCredentialLocker on a nonexistent locker fails with ResourceNotFoundException",
28 () =>
29 Effect.gen(function* () {
30 const error = yield* Effect.flip(
31 pin(
32 mi.getCredentialLocker({
33 Identifier: "alchemynonexistentlockerprobe",
34 }),
35 ),
36 );
37 expect(error._tag).toBe("ResourceNotFoundException");
38 }),
39 { tags: ["provider:aws", "provider:aws:iotmanagedintegrations", "live"] },
40);
41
42class LockerStillExists extends Data.TaggedError("LockerStillExists")<{
43 readonly credentialLockerId: string;
44}> {}
45
46const assertLockerGone = (credentialLockerId: string) =>
47 mi.getCredentialLocker({ Identifier: credentialLockerId }).pipe(
48 Effect.flatMap(() =>
49 Effect.fail(new LockerStillExists({ credentialLockerId })),
50 ),
51 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
52 Effect.retry({
53 while: (e) => e._tag === "LockerStillExists",
54 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
55 }),
56 );
57
58// Full lifecycle is gated: the service only exists in a few regions. Run with
59// AWS_TEST_IOT_MI=1 and an AWS profile configured in a supported region.
60test.provider.skipIf(!process.env.AWS_TEST_IOT_MI)(
61 "create credential locker, verify, update tags, destroy",
62 (stack) =>
63 Effect.gen(function* () {
64 yield* stack.destroy();
65
66 const { locker } = yield* stack.deploy(
67 Effect.gen(function* () {
68 const locker = yield* CredentialLocker("Locker", {
69 tags: { fixture: "iot-mi-credential-locker" },
70 });
71 return { locker };
72 }),
73 );
74
75 expect(locker.credentialLockerId).toBeDefined();
76 expect(locker.credentialLockerArn).toContain(":credential-locker/");
77 expect(locker.credentialLockerName).toBeDefined();
78 expect(locker.tags.fixture).toBe("iot-mi-credential-locker");
79
80 // Out-of-band verification via distilled.
81 const observed = yield* mi.getCredentialLocker({
82 Identifier: locker.credentialLockerId,
83 });
84 expect(observed.Arn).toBe(locker.credentialLockerArn);
85 expect(observed.Tags?.fixture).toBe("iot-mi-credential-locker");
86
87 // Update tags in place (no replacement — same name).
88 const { locker: updated } = yield* stack.deploy(
89 Effect.gen(function* () {
90 const locker = yield* CredentialLocker("Locker", {
91 tags: { fixture: "iot-mi-credential-locker", phase: "two" },
92 });
93 return { locker };
94 }),
95 );
96 expect(updated.credentialLockerId).toBe(locker.credentialLockerId);
97 const retagged = yield* mi.getCredentialLocker({
98 Identifier: locker.credentialLockerId,
99 });
100 expect(retagged.Tags?.phase).toBe("two");
101
102 // Replacement: an explicit name change replaces the locker.
103 const { locker: replaced } = yield* stack.deploy(
104 Effect.gen(function* () {
105 const locker = yield* CredentialLocker("Locker", {
106 name: "alchemy-iot-mi-locker-replacement",
107 tags: { fixture: "iot-mi-credential-locker" },
108 });
109 return { locker };
110 }),
111 );
112 expect(replaced.credentialLockerId).not.toBe(locker.credentialLockerId);
113 expect(replaced.credentialLockerName).toBe(
114 "alchemy-iot-mi-locker-replacement",
115 );
116 yield* assertLockerGone(locker.credentialLockerId);
117
118 yield* stack.destroy();
119 yield* assertLockerGone(replaced.credentialLockerId);
120 }),
121 {
122 tags: ["provider:aws", "provider:aws:iotmanagedintegrations", "live"],
123 timeout: 120_000,
124 },
125);