| 1 | import * as AWS from "@/AWS"; |
| 2 | import { OpenIDConnectProvider } from "@/AWS/IAM"; |
| 3 | import * as Provider from "@/Provider"; |
| 4 | import { isResourceState, State, type ResourceState } from "@/State"; |
| 5 | import * as Test from "@/Test/Alchemy"; |
| 6 | import * as IAM from "@distilled.cloud/aws/iam"; |
| 7 | import { describe, expect } from "alchemy-test"; |
| 8 | import * as Effect from "effect/Effect"; |
| 9 | import { testOidcListUrl, testOidcThumbprintA } from "./fixtures.ts"; |
| 10 | |
| 11 | const { 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. |
| 16 | const testOidcWedgedUrl = "https://example.com/alchemy-oidc-wedged"; |
| 17 | |
| 18 | describe( |
| 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 | ); |