1import * as ACME from "@/ACME";
2import * as Test from "@/Test/Alchemy";
3import * as acme from "@distilled.cloud/acme/acme";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Redacted from "effect/Redacted";
7import * as Result from "effect/Result";
8
9const { test } = Test.make({ providers: ACME.providers() });
10const ca = ACME.LetsEncryptStaging;
11
12const 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
20test.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
67test.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);