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 { describe, expect } from "alchemy-test";
7import * as Data from "effect/Data";
8import * as Effect from "effect/Effect";
9import * as Redacted from "effect/Redacted";
10import { MinimumLogLevel } from "effect/References";
11import * as Schedule from "effect/Schedule";
12const { test } = Test.make({ providers: Cloudflare.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19describe.skip(
20 "AccountApiToken",
21 { tags: ["provider:cloudflare", "provider:cloudflare:apitoken", "live"] },
22 () => {
23 test.provider(
24 "create and delete account token with default props",
25 (stack) =>
26 Effect.gen(function* () {
27 const { accountId } = yield* yield* CloudflareEnvironment;
28
29 yield* stack.destroy();
30
31 const token = yield* stack.deploy(
32 Effect.gen(function* () {
33 return yield* Cloudflare.ApiToken.AccountApiToken(
34 "DefaultToken",
35 {
36 policies: [
37 {
38 effect: "allow",
39 permissionGroups: ["Workers Scripts Read"],
40 resources: {
41 [`com.cloudflare.api.account.${accountId}`]: "*",
42 },
43 },
44 ],
45 },
46 );
47 }),
48 );
49
50 expect(token.tokenId).toBeDefined();
51 expect(token.name).toBeDefined();
52 expect(token.status).toEqual("active");
53 expect(Redacted.value(token.value)).toMatch(/.+/);
54
55 const actualToken = yield* accounts.getToken({
56 accountId,
57 tokenId: token.tokenId,
58 });
59 expect(actualToken.id).toEqual(token.tokenId);
60 expect(actualToken.name).toEqual(token.name);
61
62 yield* stack.destroy();
63
64 yield* waitForTokenToBeDeleted(token.tokenId, accountId);
65 }).pipe(logLevel),
66 { tags: ["provider:cloudflare:account"] },
67 );
68
69 test.provider(
70 "create, update, delete account token",
71 (stack) =>
72 Effect.gen(function* () {
73 const { accountId } = yield* yield* CloudflareEnvironment;
74
75 yield* stack.destroy();
76
77 const token = yield* stack.deploy(
78 Effect.gen(function* () {
79 return yield* Cloudflare.ApiToken.AccountApiToken("UpdateToken", {
80 name: "alchemy-test-acct-update-initial",
81 policies: [
82 {
83 effect: "allow",
84 permissionGroups: ["Workers Scripts Read"],
85 resources: {
86 [`com.cloudflare.api.account.${accountId}`]: "*",
87 },
88 },
89 ],
90 });
91 }),
92 );
93
94 expect(token.name).toEqual("alchemy-test-acct-update-initial");
95 const initialValue = Redacted.value(token.value);
96
97 const updated = yield* stack.deploy(
98 Effect.gen(function* () {
99 return yield* Cloudflare.ApiToken.AccountApiToken("UpdateToken", {
100 name: "alchemy-test-acct-update-renamed",
101 policies: [
102 {
103 effect: "allow",
104 permissionGroups: [
105 "Workers Scripts Read",
106 "Workers KV Storage Read",
107 ],
108 resources: {
109 [`com.cloudflare.api.account.${accountId}`]: "*",
110 },
111 },
112 ],
113 });
114 }),
115 );
116
117 expect(updated.tokenId).toEqual(token.tokenId);
118 expect(updated.name).toEqual("alchemy-test-acct-update-renamed");
119 expect(Redacted.value(updated.value)).toEqual(initialValue);
120
121 const actual = yield* accounts.getToken({
122 accountId,
123 tokenId: updated.tokenId,
124 });
125 expect(actual.name).toEqual("alchemy-test-acct-update-renamed");
126 expect(actual.policies?.[0]?.permissionGroups.length).toEqual(2);
127
128 yield* stack.destroy();
129
130 yield* waitForTokenToBeDeleted(token.tokenId, accountId);
131 }).pipe(logLevel),
132 { tags: ["provider:cloudflare:account"] },
133 );
134
135 test.provider("noop when account token props unchanged", (stack) =>
136 Effect.gen(function* () {
137 const { accountId } = yield* yield* CloudflareEnvironment;
138
139 yield* stack.destroy();
140
141 const props = {
142 name: "alchemy-test-acct-noop",
143 policies: [
144 {
145 effect: "allow" as const,
146 permissionGroups: ["Workers Scripts Read" as const],
147 resources: {
148 [`com.cloudflare.api.account.${accountId}`]: "*",
149 },
150 },
151 ],
152 };
153
154 const first = yield* stack.deploy(
155 Effect.gen(function* () {
156 return yield* Cloudflare.ApiToken.AccountApiToken(
157 "NoopToken",
158 props,
159 );
160 }),
161 );
162
163 const second = yield* stack.deploy(
164 Effect.gen(function* () {
165 return yield* Cloudflare.ApiToken.AccountApiToken(
166 "NoopToken",
167 props,
168 );
169 }),
170 );
171
172 expect(second.tokenId).toEqual(first.tokenId);
173 expect(Redacted.value(second.value)).toEqual(
174 Redacted.value(first.value),
175 );
176
177 yield* stack.destroy();
178 }).pipe(logLevel),
179 );
180
181 const waitForTokenToBeDeleted = Effect.fn(function* (
182 tokenId: string,
183 accountId: string,
184 ) {
185 yield* accounts.getToken({ accountId, tokenId }).pipe(
186 Effect.flatMap(() => Effect.fail(new TokenStillExists())),
187 Effect.retry({
188 while: (e): e is TokenStillExists => e instanceof TokenStillExists,
189 schedule: Schedule.max([
190 Schedule.exponential(200),
191 Schedule.recurs(8),
192 ]),
193 }),
194 Effect.catchTag("TokenStillExists", () =>
195 Effect.die(
196 `Cloudflare API token ${tokenId} was not deleted after retries`,
197 ),
198 ),
199 Effect.catchTag("TokenNotFound", () => Effect.void),
200 Effect.catchTag("InvalidRoute", () => Effect.void),
201 );
202 });
203 },
204);
205class TokenStillExists extends Data.TaggedError("TokenStillExists") {}
206
207describe(
208 "AccountApiToken list",
209 { tags: ["provider:cloudflare", "provider:cloudflare:apitoken", "live"] },
210 () => {
211 test.provider("list enumerates the deployed account token", (stack) =>
212 Effect.gen(function* () {
213 const { accountId } = yield* yield* CloudflareEnvironment;
214
215 yield* stack.destroy();
216
217 const token = yield* stack.deploy(
218 Effect.gen(function* () {
219 return yield* Cloudflare.ApiToken.AccountApiToken("ListToken", {
220 name: "alchemy-test-acct-list",
221 policies: [
222 {
223 effect: "allow",
224 permissionGroups: ["Workers Scripts Read"],
225 resources: {
226 [`com.cloudflare.api.account.${accountId}`]: "*",
227 },
228 },
229 ],
230 });
231 }),
232 );
233
234 const provider = yield* Provider.findProvider(
235 Cloudflare.ApiToken.AccountApiToken,
236 );
237 const all = yield* provider.list();
238
239 expect(all.some((t) => t.tokenId === token.tokenId)).toBe(true);
240 const found = all.find((t) => t.tokenId === token.tokenId)!;
241 expect(found.name).toEqual(token.name);
242 expect(found.accountId).toEqual(accountId);
243
244 yield* stack.destroy();
245 }).pipe(logLevel),
246 );
247 },
248);