1import * as AWS from "@/AWS";
2import { ResourceShare } from "@/AWS/RAM";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as ram from "@distilled.cloud/aws/ram";
6import { expect } from "alchemy-test";
7import * as Data from "effect/Data";
8import * as Effect from "effect/Effect";
9import { MinimumLogLevel } from "effect/References";
10import * as Schedule from "effect/Schedule";
11
12const { test } = Test.make({ providers: AWS.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19// A well-known AWS documentation placeholder account ID; not assigned to any
20// real account, so sharing to it is inert but exercises the PRINCIPAL path.
21const EXTERNAL_PRINCIPAL = "123456789012";
22
23class ShareStillLive extends Data.TaggedError("ShareStillLive") {}
24class PrincipalStillAssociated extends Data.TaggedError(
25 "PrincipalStillAssociated",
26) {}
27
28const readShare = Effect.fn(function* (arn: string) {
29 const shares = yield* ram
30 .getResourceShares({ resourceOwner: "SELF", resourceShareArns: [arn] })
31 .pipe(
32 Effect.map((r) => r.resourceShares ?? []),
33 Effect.catchTag("UnknownResourceException", () => Effect.succeed([])),
34 );
35 return shares.find(
36 (s) =>
37 s.resourceShareArn === arn &&
38 s.status !== "DELETING" &&
39 s.status !== "DELETED",
40 );
41});
42
43const readPrincipals = Effect.fn(function* (arn: string) {
44 const associations = yield* ram
45 .getResourceShareAssociations({
46 resourceShareArns: [arn],
47 associationType: "PRINCIPAL",
48 })
49 .pipe(
50 Effect.map((r) => r.resourceShareAssociations ?? []),
51 Effect.catchTag("UnknownResourceException", () => Effect.succeed([])),
52 );
53 return associations
54 .filter((a) => a.status === "ASSOCIATED" || a.status === "ASSOCIATING")
55 .map((a) => a.associatedEntity);
56});
57
58const assertDeleted = Effect.fn(function* (arn: string) {
59 yield* readShare(arn).pipe(
60 Effect.flatMap((s) =>
61 s === undefined ? Effect.void : Effect.fail(new ShareStillLive()),
62 ),
63 Effect.retry({
64 while: (e) => e instanceof ShareStillLive,
65 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(15)]),
66 }),
67 );
68});
69
70const shareStack = (props: {
71 principals?: string[];
72 tags?: Record<string, string>;
73}) =>
74 Effect.gen(function* () {
75 const share = yield* ResourceShare("TestShare", {
76 allowExternalPrincipals: true,
77 principals: props.principals,
78 tags: props.tags,
79 });
80 return { share };
81 });
82
83test.provider(
84 "create, associate principal, update tags, disassociate, delete",
85 (stack) =>
86 Effect.gen(function* () {
87 yield* stack.destroy();
88
89 // 1. Create with an external principal + tags.
90 const { share } = yield* stack.deploy(
91 shareStack({
92 principals: [EXTERNAL_PRINCIPAL],
93 tags: { team: "platform" },
94 }),
95 );
96
97 expect(share.resourceShareArn).toMatch(/^arn:aws:ram:/);
98 expect(share.allowExternalPrincipals).toBe(true);
99 const arn = share.resourceShareArn;
100
101 // 2. Out-of-band: the share is ACTIVE with our tags.
102 const live = yield* readShare(arn);
103 expect(live?.resourceShareArn).toEqual(arn);
104 expect(live?.allowExternalPrincipals).toBe(true);
105 expect(
106 live?.tags?.some((t) => t.key === "team" && t.value === "platform"),
107 ).toBe(true);
108 expect(live?.tags?.some((t) => t.key === "alchemy::id")).toBe(true);
109
110 // 3. Out-of-band: the PRINCIPAL association exists (may still be
111 // ASSOCIATING immediately after create).
112 yield* Effect.gen(function* () {
113 const principals = yield* readPrincipals(arn);
114 expect(principals).toContain(EXTERNAL_PRINCIPAL);
115 }).pipe(
116 Effect.retry({
117 schedule: Schedule.max([
118 Schedule.exponential(500),
119 Schedule.recurs(10),
120 ]),
121 }),
122 );
123
124 // 4. Update in place: drop the principal, add a tag. ARN is stable.
125 const { share: updated } = yield* stack.deploy(
126 shareStack({
127 principals: [],
128 tags: { team: "platform", env: "prod" },
129 }),
130 );
131 expect(updated.resourceShareArn).toEqual(arn);
132
133 // 5. Out-of-band: new tag present, principal disassociated.
134 const live2 = yield* readShare(arn);
135 expect(
136 live2?.tags?.some((t) => t.key === "env" && t.value === "prod"),
137 ).toBe(true);
138
139 yield* readPrincipals(arn).pipe(
140 Effect.flatMap((principals) =>
141 principals.includes(EXTERNAL_PRINCIPAL)
142 ? Effect.fail(new PrincipalStillAssociated())
143 : Effect.void,
144 ),
145 Effect.retry({
146 while: (e) => e instanceof PrincipalStillAssociated,
147 schedule: Schedule.max([
148 Schedule.exponential(500),
149 Schedule.recurs(12),
150 ]),
151 }),
152 );
153
154 // 6. Delete and confirm gone.
155 yield* stack.destroy();
156 yield* assertDeleted(arn);
157 }).pipe(logLevel),
158 { tags: ["provider:aws", "provider:aws:ram", "live"], timeout: 240_000 },
159);
160
161test.provider(
162 "list enumerates the deployed resource share",
163 (stack) =>
164 Effect.gen(function* () {
165 yield* stack.destroy();
166
167 const { share } = yield* stack.deploy(shareStack({}));
168
169 const provider = yield* Provider.findProvider(ResourceShare);
170 const all = yield* provider.list();
171 expect(
172 all.some((x) => x.resourceShareArn === share.resourceShareArn),
173 ).toBe(true);
174
175 yield* stack.destroy();
176 yield* assertDeleted(share.resourceShareArn);
177 }).pipe(logLevel),
178 { tags: ["provider:aws", "provider:aws:ram", "live"], timeout: 240_000 },
179);