1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as logs from "@distilled.cloud/cloudflare/logs";
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// The Customer Metadata Boundary is part of the Data Localization Suite and
19// requires an Enterprise plan. On the standard testing account every
20// `/logs/control/cmb/config` call fails with Cloudflare error code 10000
21// ("Unauthorized"), surfaced as the typed `LogsControlNotAuthorized` error.
22// The full lifecycle test below is gated behind an entitled account
23// supplied via env.
24const entitled = !!process.env.CLOUDFLARE_TEST_LOGS_CONTROL;
25
26// The scoped API token the test harness mints propagates eventually-
27// consistently across Cloudflare's edge — ride out 403 blips (`Forbidden`,
28// declared in the distilled error union via patches) on out-of-band calls.
29const getCmb = (accountId: string) =>
30 logs.getControlCmbConfig({ accountId }).pipe(
31 Effect.retry({
32 while: (e) => e._tag === "Forbidden",
33 schedule: Schedule.exponential("500 millis"),
34 times: 8,
35 }),
36 );
37
38test.provider.skipIf(entitled)(
39 "surfaces the typed LogsControlNotAuthorized error on unentitled accounts",
40 (stack) =>
41 Effect.gen(function* () {
42 const { accountId } = yield* yield* CloudflareEnvironment;
43
44 yield* stack.destroy();
45
46 // The testing account has no CMB / Data Localization entitlement —
47 // both reads and writes must fail with the typed authorization tag
48 // (Cloudflare error code 10000).
49 const readError = yield* logs
50 .getControlCmbConfig({ accountId })
51 .pipe(Effect.flip);
52 expect(readError._tag).toEqual("LogsControlNotAuthorized");
53
54 const writeError = yield* logs
55 .createControlCmbConfig({ accountId, regions: "eu" })
56 .pipe(Effect.flip);
57 expect(writeError._tag).toEqual("LogsControlNotAuthorized");
58
59 yield* stack.destroy();
60 }).pipe(logLevel),
61 { tags: ["provider:cloudflare", "provider:cloudflare:logscontrol", "live"] },
62);
63
64test.provider.skipIf(!entitled)(
65 "create, verify out-of-band, update in place, destroy, and wait until gone",
66 (stack) =>
67 Effect.gen(function* () {
68 const { accountId } = yield* yield* CloudflareEnvironment;
69
70 yield* stack.destroy();
71
72 // Create — pin the CMB region.
73 const created = yield* stack.deploy(
74 Cloudflare.LogsControl.CmbConfig("Cmb", {
75 regions: "eu",
76 }),
77 );
78 expect(created.accountId).toEqual(accountId);
79 expect(created.regions).toEqual("eu");
80
81 const live = yield* getCmb(accountId);
82 expect(live.regions).toEqual("eu");
83
84 // Update in place — POST is a full upsert.
85 const updated = yield* stack.deploy(
86 Cloudflare.LogsControl.CmbConfig("Cmb", {
87 regions: "eu",
88 allowOutOfRegionAccess: true,
89 }),
90 );
91 expect(updated.regions).toEqual("eu");
92 expect(updated.allowOutOfRegionAccess).toEqual(true);
93
94 const liveUpdated = yield* getCmb(accountId);
95 expect(liveUpdated.allowOutOfRegionAccess).toEqual(true);
96
97 yield* stack.destroy();
98
99 // Destroy removed the config — the account reads back unconfigured,
100 // surfaced either as an empty result or the typed CmbConfigNotFound.
101 const gone = yield* getCmb(accountId).pipe(
102 Effect.map((config) => config.regions ?? undefined),
103 Effect.catchTag("CmbConfigNotFound", () => Effect.succeed(undefined)),
104 Effect.repeat({
105 schedule: Schedule.spaced("2 seconds"),
106 until: (regions) => regions === undefined,
107 times: 10,
108 }),
109 );
110 expect(gone).toBeUndefined();
111 }).pipe(logLevel),
112 {
113 tags: ["provider:cloudflare", "provider:cloudflare:logscontrol", "live"],
114 timeout: 120_000,
115 },
116);
117
118// Account-singleton `list()`: there is no account-wide collection API, so
119// `list()` reads the single CMB config and returns a one-element array when
120// set, `[]` when unconfigured. On unentitled accounts the underlying GET
121// fails with the typed `LogsControlNotAuthorized` error (Cloudflare code
122// 10000), which `list()` deliberately tolerates (returns `[]`) so that
123// account-wide enumeration / `nuke` never blows up on an unentitled account.
124// The raw-op probe test above already pins the typed tag; here we assert the
125// nuke-safe empty result.
126test.provider.skipIf(entitled)(
127 "list tolerates the typed LogsControlNotAuthorized error on unentitled accounts",
128 (stack) =>
129 Effect.gen(function* () {
130 yield* stack.destroy();
131
132 const provider = yield* Provider.findProvider(
133 Cloudflare.LogsControl.CmbConfig,
134 );
135 const all = yield* provider.list();
136 expect(all).toEqual([]);
137
138 yield* stack.destroy();
139 }).pipe(logLevel),
140 { tags: ["provider:cloudflare", "provider:cloudflare:logscontrol", "live"] },
141);
142
143// Live `list()` on an entitled account: deploy the singleton, then assert
144// it appears in the enumerated result as a well-typed Attributes array.
145test.provider.skipIf(!entitled)(
146 "list enumerates the account CMB config singleton",
147 (stack) =>
148 Effect.gen(function* () {
149 const { accountId } = yield* yield* CloudflareEnvironment;
150
151 yield* stack.destroy();
152
153 yield* stack.deploy(
154 Cloudflare.LogsControl.CmbConfig("Cmb", {
155 regions: "eu",
156 }),
157 );
158
159 const provider = yield* Provider.findProvider(
160 Cloudflare.LogsControl.CmbConfig,
161 );
162 const all = yield* provider.list();
163
164 expect(all.length).toEqual(1);
165 expect(all[0]?.accountId).toEqual(accountId);
166 expect(all[0]?.regions).toEqual("eu");
167
168 yield* stack.destroy();
169
170 // After destroy the singleton is unconfigured — `list()` returns `[]`.
171 const empty = yield* provider.list();
172 expect(empty).toEqual([]);
173 }).pipe(logLevel),
174 {
175 tags: ["provider:cloudflare", "provider:cloudflare:logscontrol", "live"],
176 timeout: 120_000,
177 },
178);