| 1 | import * as ACME from "@/ACME"; |
| 2 | import * as Test from "@/Test/Alchemy"; |
| 3 | import * as acme from "@distilled.cloud/acme/acme"; |
| 4 | import { expect } from "alchemy-test"; |
| 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as Redacted from "effect/Redacted"; |
| 7 | import * as Result from "effect/Result"; |
| 8 | |
| 9 | const { test } = Test.make({ providers: ACME.providers() }); |
| 10 | const ca = ACME.LetsEncryptStaging; |
| 11 | |
| 12 | const lookup = (privateKey: Redacted.Redacted<string>) => |
| 13 | acme |
| 14 | .newAccount({ onlyReturnExisting: true }) |
| 15 | .pipe( |
| 16 | Effect.provide(ACME.accountLayer({ ca, accountKey: privateKey })), |
| 17 | Effect.result, |
| 18 | ); |
| 19 | |
| 20 | test.provider( |
| 21 | "creates an account, keeps its key, synchronizes contacts and deactivates", |
| 22 | (stack) => |
| 23 | Effect.gen(function* () { |
| 24 | yield* stack.destroy(); |
| 25 | const program = (contact?: string[]) => |
| 26 | ACME.Account("Account", { |
| 27 | ca, |
| 28 | contact, |
| 29 | termsOfServiceAgreed: true, |
| 30 | }); |
| 31 | const first = yield* stack.deploy(program(["mailto:ops@alchemy.run"])); |
| 32 | expect(first.directoryUrl).toBe(ca.directoryUrl); |
| 33 | expect(first.accountUrl).toContain( |
| 34 | "acme-staging-v02.api.letsencrypt.org", |
| 35 | ); |
| 36 | expect(first.status).toBe("valid"); |
| 37 | expect(first.keyAlgorithm).toBe("ES256"); |
| 38 | expect(Redacted.isRedacted(first.privateKey)).toBe(true); |
| 39 | const found = yield* lookup(first.privateKey); |
| 40 | expect(Result.isSuccess(found)).toBe(true); |
| 41 | if (Result.isSuccess(found)) |
| 42 | expect(found.success.location).toBe(first.accountUrl); |
| 43 | |
| 44 | const again = yield* stack.deploy(program(["mailto:ops@alchemy.run"])); |
| 45 | expect(again.accountUrl).toBe(first.accountUrl); |
| 46 | expect( |
| 47 | Redacted.value(again.privateKey) === Redacted.value(first.privateKey), |
| 48 | ).toBe(true); |
| 49 | |
| 50 | const updated = yield* stack.deploy(program()); |
| 51 | expect(updated.accountUrl).toBe(first.accountUrl); |
| 52 | const observed = yield* lookup(first.privateKey); |
| 53 | expect(Result.isSuccess(observed)).toBe(true); |
| 54 | if (Result.isSuccess(observed)) |
| 55 | expect(observed.success.contact ?? []).toEqual([]); |
| 56 | yield* stack.destroy(); |
| 57 | const gone = yield* lookup(first.privateKey); |
| 58 | expect(Result.isFailure(gone)).toBe(true); |
| 59 | if (Result.isFailure(gone)) |
| 60 | expect(["AcmeUnauthorized", "AcmeAccountDoesNotExist"]).toContain( |
| 61 | gone.failure._tag, |
| 62 | ); |
| 63 | }), |
| 64 | { tags: ["provider:acme", "provider:acme:account", "live"], timeout: 90_000 }, |
| 65 | ); |
| 66 | |
| 67 | test.provider( |
| 68 | "changing the key algorithm replaces and deactivates the old account", |
| 69 | (stack) => |
| 70 | Effect.gen(function* () { |
| 71 | yield* stack.destroy(); |
| 72 | const first = yield* stack.deploy( |
| 73 | ACME.Account("Account", { ca, termsOfServiceAgreed: true }), |
| 74 | ); |
| 75 | const replaced = yield* stack.deploy( |
| 76 | ACME.Account("Account", { |
| 77 | ca, |
| 78 | termsOfServiceAgreed: true, |
| 79 | keyAlgorithm: "RS256", |
| 80 | }), |
| 81 | ); |
| 82 | expect(replaced.keyAlgorithm).toBe("RS256"); |
| 83 | expect(replaced.accountUrl).not.toBe(first.accountUrl); |
| 84 | expect( |
| 85 | (JSON.parse(Redacted.value(replaced.privateKey)) as { kty: string }) |
| 86 | .kty, |
| 87 | ).toBe("RSA"); |
| 88 | expect(Result.isFailure(yield* lookup(first.privateKey))).toBe(true); |
| 89 | yield* stack.destroy(); |
| 90 | expect(Result.isFailure(yield* lookup(replaced.privateKey))).toBe(true); |
| 91 | }), |
| 92 | { tags: ["provider:acme", "provider:acme:account", "live"], timeout: 90_000 }, |
| 93 | ); |