| 1 | import * as AWS from "@/AWS"; |
| 2 | import { Bucket } from "@/AWS/S3"; |
| 3 | import { TrustStore } from "@/AWS/ELBv2"; |
| 4 | import * as Test from "@/Test/Alchemy"; |
| 5 | import * as elbv2 from "@distilled.cloud/aws/elastic-load-balancing-v2"; |
| 6 | import * as s3 from "@distilled.cloud/aws/s3"; |
| 7 | import { expect } from "alchemy-test"; |
| 8 | import * as Effect from "effect/Effect"; |
| 9 | import { MinimumLogLevel } from "effect/References"; |
| 10 | |
| 11 | const { test } = Test.make({ providers: AWS.providers() }); |
| 12 | |
| 13 | const logLevel = Effect.provideService( |
| 14 | MinimumLogLevel, |
| 15 | process.env.DEBUG ? "Debug" : "Info", |
| 16 | ); |
| 17 | |
| 18 | // A self-signed CA certificate generated once and checked in (never created at |
| 19 | // test time, per the fixture convention). X.509 v3 with basicConstraints |
| 20 | // CA:TRUE + keyCertSign — ELBv2 trust stores reject v1 certs |
| 21 | // ("The certificate version is not supported"). |
| 22 | const CA_BUNDLE_PEM = `-----BEGIN CERTIFICATE----- |
| 23 | MIIC2jCCAcKgAwIBAgIJAJyM/Dvd55qtMA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNV |
| 24 | BAMMD2FsY2hlbXktdGVzdC1jYTAeFw0yNjA2MTcwNjA5MDlaFw0zNjA2MTQwNjA5 |
| 25 | MDlaMBoxGDAWBgNVBAMMD2FsY2hlbXktdGVzdC1jYTCCASIwDQYJKoZIhvcNAQEB |
| 26 | BQADggEPADCCAQoCggEBANgo7XPCQMpyecXg2SCj6Tn6R1snlmhSA1vKGQnHoQBS |
| 27 | QA11DMpv+iFRT9s1d3izaGA4GEcxfrXOsmUBkzYIHJIYakCWdr6qcUXs6lS2uhnZ |
| 28 | qcyR0CamDtHTqAxRKEK+QPaISoxyD3BIwQqE0I8yNzV3/6osIE513e+7tp9E+J04 |
| 29 | dBhyG5goSwR3ueqs53gQioYVp/fgLKo4MqFcsA3p7anEE9hyeq1Q/lGAXxQwZmXT |
| 30 | 3kQli/JjMoF8OfccpA3aBx9Y2aDTCU8HXTscVYmPSHbnTGkTARGBwnag+Jwq5Uni |
| 31 | YvM2OeDUPwvszgpi3JgiblZQhZQAy4/MeNhmE8qgIa8CAwEAAaMjMCEwDwYDVR0T |
| 32 | AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggEBAJnp |
| 33 | el0xBbL/eQY87evhy0o+ZTHMVCdI9Uc+kDK0XPMi4hc5OfjWNIy8u5/s33kPkNYS |
| 34 | Y5Jhm5KtGtMb9kXioCWjSi0aREA8zijGrXn1jC+0rksMQmJka63bKsJ4TjFaHMcc |
| 35 | m/xt25xX1Ssp/gWr9YX3MzbPhcn57Uu9OTtzf13F6CMv1XtRS1RKFYtkLZrhvzBR |
| 36 | WPdos3xvn3D0Fjd5H5AgVKTkeb2YPhINfN4jyzn3J09teKZpNN/qHTAQewIh2FnO |
| 37 | MeplcuT3eQVUZNTBelvUE7VKHe11AUc8TkvVMS/XOFeN6OHAJtq08EegbTcjwz9Z |
| 38 | lyGGetkNMmdhGRV6AlY= |
| 39 | -----END CERTIFICATE----- |
| 40 | `; |
| 41 | |
| 42 | // Idempotent out-of-band delete for the probe trust store below. The probe |
| 43 | // create is EXPECTED to fail, but if it ever unexpectedly succeeded the store |
| 44 | // would live outside the stack — so cleanup must be guaranteed. Not-found is |
| 45 | // success; any other error is a defect, so the error channel is `never` and |
| 46 | // this is a valid `Effect.ensuring` finalizer. |
| 47 | const deleteTrustStoreByNameIdempotent = (name: string) => |
| 48 | elbv2.describeTrustStores({ Names: [name] }).pipe( |
| 49 | Effect.flatMap((r) => |
| 50 | Effect.forEach( |
| 51 | (r.TrustStores ?? []).flatMap((t) => |
| 52 | t.TrustStoreArn ? [t.TrustStoreArn] : [], |
| 53 | ), |
| 54 | (arn) => elbv2.deleteTrustStore({ TrustStoreArn: arn }), |
| 55 | ), |
| 56 | ), |
| 57 | Effect.catchTag("TrustStoreNotFoundException", () => Effect.void), |
| 58 | Effect.orDie, |
| 59 | Effect.asVoid, |
| 60 | ); |
| 61 | |
| 62 | // Fast unconditional probe: createTrustStore against a non-existent bundle |
| 63 | // must surface a typed error (not an untyped catch-all). Proves both the |
| 64 | // resource wiring and the distilled typed-error path. |
| 65 | test.provider( |
| 66 | "trust store create with missing bundle returns a typed error", |
| 67 | (stack) => |
| 68 | Effect.gen(function* () { |
| 69 | yield* stack.destroy(); |
| 70 | |
| 71 | const probeName = |
| 72 | `alchemy-mtls-probe-${stack.name.replace(/[^a-zA-Z0-9]/g, "")}`.slice( |
| 73 | 0, |
| 74 | 32, |
| 75 | ); |
| 76 | |
| 77 | // Pre-clean: reclaim a probe trust store leaked by a prior crashed run |
| 78 | // (a leftover would turn the expected error into DuplicateTrustStoreName). |
| 79 | yield* deleteTrustStoreByNameIdempotent(probeName); |
| 80 | |
| 81 | yield* Effect.gen(function* () { |
| 82 | const result = yield* elbv2 |
| 83 | .createTrustStore({ |
| 84 | Name: probeName, |
| 85 | CaCertificatesBundleS3Bucket: "alchemy-no-such-bucket-elbv2-probe", |
| 86 | CaCertificatesBundleS3Key: "missing.pem", |
| 87 | }) |
| 88 | .pipe(Effect.flip); |
| 89 | |
| 90 | // AWS rejects a missing/inaccessible bundle with one of these typed tags. |
| 91 | expect( |
| 92 | [ |
| 93 | "CaCertificatesBundleNotFoundException", |
| 94 | "InvalidCaCertificatesBundleException", |
| 95 | ].includes(result._tag), |
| 96 | ).toBe(true); |
| 97 | }).pipe( |
| 98 | // Guaranteed cleanup if the create ever unexpectedly succeeds. |
| 99 | Effect.ensuring(deleteTrustStoreByNameIdempotent(probeName)), |
| 100 | ); |
| 101 | |
| 102 | yield* stack.destroy(); |
| 103 | }).pipe(logLevel), |
| 104 | { tags: ["provider:aws", "provider:aws:elbv2", "live"], timeout: 120_000 }, |
| 105 | ); |
| 106 | |
| 107 | // Full mTLS-verify lifecycle: upload a CA bundle to a stack-owned bucket, |
| 108 | // create an ACTIVE trust store, then destroy. Runs ungated — verified live on |
| 109 | // the standard testing account (completes in a few seconds). |
| 110 | test.provider( |
| 111 | "trust store full lifecycle from an uploaded CA bundle", |
| 112 | (stack) => |
| 113 | Effect.gen(function* () { |
| 114 | yield* stack.destroy(); |
| 115 | |
| 116 | const deployed = yield* stack.deploy( |
| 117 | Effect.gen(function* () { |
| 118 | const bucket = yield* Bucket("TsBucket", { forceDestroy: true }); |
| 119 | return { bucketName: bucket.bucketName }; |
| 120 | }), |
| 121 | ); |
| 122 | |
| 123 | const key = "ca-bundle.pem"; |
| 124 | yield* s3.putObject({ |
| 125 | Bucket: deployed.bucketName, |
| 126 | Key: key, |
| 127 | Body: CA_BUNDLE_PEM, |
| 128 | ContentType: "application/x-pem-file", |
| 129 | }); |
| 130 | |
| 131 | const ts = yield* stack.deploy( |
| 132 | Effect.gen(function* () { |
| 133 | const bucket = yield* Bucket("TsBucket", { forceDestroy: true }); |
| 134 | const trustStore = yield* TrustStore("TsStore", { |
| 135 | caCertificatesBundleS3Bucket: bucket.bucketName, |
| 136 | caCertificatesBundleS3Key: key, |
| 137 | }); |
| 138 | return { trustStore }; |
| 139 | }), |
| 140 | ); |
| 141 | |
| 142 | expect(ts.trustStore.status).toBe("ACTIVE"); |
| 143 | expect(ts.trustStore.numberOfCaCertificates).toBeGreaterThanOrEqual(1); |
| 144 | |
| 145 | const observed = yield* elbv2 |
| 146 | .describeTrustStores({ |
| 147 | TrustStoreArns: [ts.trustStore.trustStoreArn], |
| 148 | }) |
| 149 | .pipe(Effect.map((r) => r.TrustStores?.[0])); |
| 150 | expect(observed?.Status).toBe("ACTIVE"); |
| 151 | |
| 152 | yield* stack.destroy(); |
| 153 | |
| 154 | const after = yield* elbv2 |
| 155 | .describeTrustStores({ |
| 156 | TrustStoreArns: [ts.trustStore.trustStoreArn], |
| 157 | }) |
| 158 | .pipe( |
| 159 | Effect.map((r) => r.TrustStores?.length ?? 0), |
| 160 | Effect.catchTag("TrustStoreNotFoundException", () => |
| 161 | Effect.succeed(0), |
| 162 | ), |
| 163 | ); |
| 164 | expect(after).toBe(0); |
| 165 | }).pipe(logLevel), |
| 166 | { |
| 167 | tags: ["provider:aws", "provider:aws:elbv2", "provider:aws:s3", "live"], |
| 168 | timeout: 600_000, |
| 169 | }, |
| 170 | ); |