1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as accounts from "@distilled.cloud/cloudflare/accounts";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: Cloudflare.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18// Creating accounts is tenant/partner-entitled. The standard testing
19// account is NOT entitled, so by default only the read paths and the typed
20// entitlement-rejection path run. Set CLOUDFLARE_TENANT_TEST=1 under tenant
21// credentials to exercise the full CRUD lifecycle on a real subaccount.
22const tenantEntitled = !!process.env.CLOUDFLARE_TENANT_TEST;
23
24// A syntactically valid account id that cannot exist.
25const missingAccountId = "00000000000000000000000000000000";
26
27test.provider(
28 "read path: getAccount observes the testing account",
29 (stack) =>
30 Effect.gen(function* () {
31 const { accountId } = yield* yield* CloudflareEnvironment;
32
33 yield* stack.destroy();
34
35 const observed = yield* accounts.getAccount({ accountId });
36 expect(observed.id).toEqual(accountId);
37 expect(observed.name).toBeTruthy();
38 expect(["standard", "enterprise"]).toContain(observed.type);
39
40 yield* stack.destroy();
41 }).pipe(logLevel),
42 { tags: ["provider:cloudflare", "provider:cloudflare:account", "live"] },
43);
44
45test.provider(
46 "inaccessible account surfaces a typed tag",
47 (stack) =>
48 Effect.gen(function* () {
49 yield* stack.destroy();
50
51 // With an account-scoped token, an account id outside the token's
52 // scope is rejected with the typed `Unauthorized` tag before routing.
53 // (A tenant-owned account that has been deleted surfaces as
54 // `InvalidRoute`, code 7003 — the tag the provider's `read` and
55 // `delete` treat as "gone".)
56 const error = yield* accounts
57 .getAccount({ accountId: missingAccountId })
58 .pipe(Effect.flip);
59 expect(error._tag).toEqual("Unauthorized");
60
61 yield* stack.destroy();
62 }).pipe(logLevel),
63 { tags: ["provider:cloudflare", "provider:cloudflare:account", "live"] },
64);
65
66// Account creation is tenant-gated, so list() is verified read-only: a
67// standard token can LIST every account it can access, and the token's own
68// testing account must appear in the exhaustively-paginated result with the
69// exact `Attributes` shape `read` produces.
70test.provider(
71 "list enumerates accessible accounts (read-only)",
72 (stack) =>
73 Effect.gen(function* () {
74 const { accountId } = yield* yield* CloudflareEnvironment;
75
76 yield* stack.destroy();
77
78 const provider = yield* Provider.findProvider(Cloudflare.Account.Account);
79 const all = yield* provider.list();
80
81 expect(all.length).toBeGreaterThan(0);
82
83 const self = all.find((a) => a.accountId === accountId);
84 expect(self).toBeDefined();
85 expect(self?.name).toBeTruthy();
86 expect(["standard", "enterprise"]).toContain(self?.type);
87 expect(typeof self?.enforceTwofactor).toEqual("boolean");
88
89 yield* stack.destroy();
90 }).pipe(logLevel),
91 { tags: ["provider:cloudflare", "provider:cloudflare:account", "live"] },
92);
93
94test.provider.skipIf(tenantEntitled)(
95 "createAccount is entitlement-gated: typed AccountCreationForbidden",
96 (stack) =>
97 Effect.gen(function* () {
98 yield* stack.destroy();
99
100 // The standard testing account lacks the tenant entitlement, so
101 // account creation fails with the typed tag (code 1002). Never
102 // touches the real testing account.
103 const error = yield* accounts
104 .createAccount({ name: "alchemy-test-entitlement-probe" })
105 .pipe(Effect.flip);
106 expect(error._tag).toEqual("AccountCreationForbidden");
107
108 yield* stack.destroy();
109 }).pipe(logLevel),
110 { tags: ["provider:cloudflare", "provider:cloudflare:account", "live"] },
111);
112
113// Full lifecycle — only under tenant credentials (CLOUDFLARE_TENANT_TEST=1).
114// Creates a brand-new subaccount; the real testing account is never mutated.
115test.provider.skipIf(!tenantEntitled)(
116 "create subaccount, update name and settings in place, delete",
117 (stack) =>
118 Effect.gen(function* () {
119 yield* stack.destroy();
120
121 const account = yield* stack.deploy(
122 Cloudflare.Account.Account("TestSubaccount", {
123 name: "alchemy-test-subaccount",
124 }),
125 );
126
127 expect(account.accountId).toBeTruthy();
128 expect(account.name).toEqual("alchemy-test-subaccount");
129 expect(account.type).toEqual("standard");
130
131 // Out-of-band verify.
132 const live = yield* accounts.getAccount({
133 accountId: account.accountId,
134 });
135 expect(live.name).toEqual("alchemy-test-subaccount");
136
137 // Rename + settings update happen in place (same account id).
138 const updated = yield* stack.deploy(
139 Cloudflare.Account.Account("TestSubaccount", {
140 name: "alchemy-test-subaccount-renamed",
141 enforceTwofactor: true,
142 }),
143 );
144 expect(updated.accountId).toEqual(account.accountId);
145 expect(updated.name).toEqual("alchemy-test-subaccount-renamed");
146 expect(updated.enforceTwofactor).toEqual(true);
147
148 yield* stack.destroy();
149
150 // Deletion is queued on Cloudflare's side; the account becomes
151 // unreadable (`InvalidRoute`) once it leaves the tenant.
152 yield* accounts.getAccount({ accountId: account.accountId }).pipe(
153 Effect.flatMap(() =>
154 Effect.fail({ _tag: "AccountNotDeleted" } as const),
155 ),
156 Effect.catchTag("InvalidRoute", () => Effect.void),
157 Effect.retry({
158 while: (e) => e._tag === "AccountNotDeleted",
159 schedule: Schedule.max([
160 Schedule.exponential("500 millis"),
161 Schedule.recurs(10),
162 ]),
163 }),
164 );
165 }).pipe(logLevel),
166 { tags: ["provider:cloudflare", "provider:cloudflare:account", "live"] },
167);