1import * as AWS from "@/AWS";
2import { OpenIDConnectProvider } from "@/AWS/IAM";
3import * as Provider from "@/Provider";
4import { isResourceState, State, type ResourceState } from "@/State";
5import * as Test from "@/Test/Alchemy";
6import * as IAM from "@distilled.cloud/aws/iam";
7import { describe, expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { testOidcListUrl, testOidcThumbprintA } from "./fixtures.ts";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13// A distinct issuer URL for the #736 recovery test — the OIDC provider ARN is
14// deterministic from the URL, so sharing a URL with another test would make
15// two tests provision (and tear down) the same physical provider.
16const testOidcWedgedUrl = "https://example.com/alchemy-oidc-wedged";
17
18describe(
19 "AWS.IAM.OpenIDConnectProvider",
20 { tags: ["provider:aws", "provider:aws:iam", "live"] },
21 () => {
22 test.provider(
23 "list enumerates the deployed OpenID Connect provider",
24 (stack) =>
25 Effect.gen(function* () {
26 yield* stack.destroy();
27
28 const deployed = yield* stack.deploy(
29 Effect.gen(function* () {
30 return yield* OpenIDConnectProvider("ListOidcProvider", {
31 url: testOidcListUrl,
32 clientIDList: ["sts.amazonaws.com"],
33 thumbprintList: [testOidcThumbprintA],
34 tags: {
35 env: "test",
36 },
37 });
38 }),
39 );
40
41 const provider = yield* Provider.findProvider(OpenIDConnectProvider);
42 const all = yield* provider.list();
43
44 expect(
45 all.some(
46 (p) =>
47 p.openIDConnectProviderArn ===
48 deployed.openIDConnectProviderArn,
49 ),
50 ).toBe(true);
51
52 const found = all.find(
53 (p) =>
54 p.openIDConnectProviderArn === deployed.openIDConnectProviderArn,
55 );
56 expect(found?.url).toBe(testOidcListUrl.replace(/^https?:\/\//, ""));
57 expect(found?.clientIDList ?? []).toContain("sts.amazonaws.com");
58
59 yield* stack.destroy();
60
61 const deleted = yield* IAM.getOpenIDConnectProvider({
62 OpenIDConnectProviderArn: deployed.openIDConnectProviderArn,
63 }).pipe(Effect.option);
64 expect(deleted._tag).toBe("None");
65 }),
66 );
67
68 test.provider(
69 "recovers a half-created provider whose creating-state lost Output-valued props (#736)",
70 (stack) =>
71 Effect.gen(function* () {
72 yield* stack.destroy();
73
74 const deployProvider = () =>
75 stack.deploy(
76 Effect.gen(function* () {
77 return yield* OpenIDConnectProvider("WedgedOidcProvider", {
78 url: testOidcWedgedUrl,
79 clientIDList: ["sts.amazonaws.com"],
80 thumbprintList: [testOidcThumbprintA],
81 });
82 }),
83 );
84
85 const created = yield* deployProvider();
86
87 // Rewrite the provider's persisted row into the wedged shape an
88 // interrupted deploy leaves behind: `creating`, no attributes, and
89 // the Output-valued `url` prop lost in the round-trip.
90 const state = yield* yield* State;
91 const stage = stack.stage;
92 const fqns = yield* state.list({ stack: stack.name, stage });
93 const rows = yield* Effect.forEach(fqns, (fqn) =>
94 state
95 .get({ stack: stack.name, stage, fqn })
96 .pipe(Effect.map((row) => ({ fqn, row }))),
97 );
98 const wedged = rows.find(
99 (r): r is { fqn: string; row: ResourceState } =>
100 isResourceState(r.row) &&
101 r.row.resourceType === "AWS.IAM.OpenIDConnectProvider",
102 );
103 if (!wedged) {
104 return yield* Effect.die(
105 new Error(
106 "no AWS.IAM.OpenIDConnectProvider state row found after deploy",
107 ),
108 );
109 }
110 yield* state.set({
111 stack: stack.name,
112 stage,
113 fqn: wedged.fqn,
114 value: {
115 ...wedged.row,
116 status: "creating",
117 attr: undefined,
118 props: {
119 ...wedged.row.props,
120 url: undefined,
121 },
122 },
123 });
124
125 // Before the fix this crashed in `read` with
126 // `TypeError: undefined is not an object (evaluating 'url.replace')`.
127 const recovered = yield* deployProvider();
128 expect(recovered.openIDConnectProviderArn).toEqual(
129 created.openIDConnectProviderArn,
130 );
131 expect(recovered.url).toEqual(created.url);
132
133 yield* stack.destroy();
134
135 const deleted = yield* IAM.getOpenIDConnectProvider({
136 OpenIDConnectProviderArn: created.openIDConnectProviderArn,
137 }).pipe(Effect.option);
138 expect(deleted._tag).toBe("None");
139 }),
140 { timeout: 240_000 },
141 );
142 },
143);