1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import * as Provider from "@/Provider";
4import * as Test from "@/Test/Alchemy";
5import * as zeroTrust from "@distilled.cloud/cloudflare/zero-trust";
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// Ride out 403 blips (`Forbidden`) while the harness-minted token
19// propagates across Cloudflare's edge.
20const getConfiguration = (accountId: string) =>
21 zeroTrust.getGatewayConfiguration({ accountId }).pipe(
22 Effect.retry({
23 while: (e) => e._tag === "Forbidden",
24 schedule: Schedule.exponential("500 millis"),
25 times: 8,
26 }),
27 );
28
29// Seed a deterministic baseline for the blocks under management. Blocks
30// that were never set on the account cannot be restored (Cloudflare's
31// PATCH ignores `null`), so the capture-and-restore assertion needs the
32// blocks to exist with known values before the stack manages them.
33const seedBaseline = (accountId: string) =>
34 zeroTrust
35 .patchGatewayConfiguration({
36 accountId,
37 settings: {
38 activityLog: { enabled: true },
39 protocolDetection: { enabled: false },
40 },
41 })
42 .pipe(
43 Effect.retry({
44 while: (e) => e._tag === "Forbidden",
45 schedule: Schedule.exponential("500 millis"),
46 times: 8,
47 }),
48 );
49
50test.provider(
51 "manage activityLog and protocolDetection, then restore on destroy",
52 (stack) =>
53 Effect.gen(function* () {
54 const { accountId } = yield* yield* CloudflareEnvironment;
55
56 yield* stack.destroy();
57
58 // Seed and capture a deterministic pre-test state out-of-band so
59 // we can verify the capture-and-restore behaviour at the end.
60 yield* seedBaseline(accountId);
61 const baseline = yield* getConfiguration(accountId);
62 const baselineActivityLog = baseline.settings?.activityLog?.enabled;
63 const baselineProtocolDetection =
64 baseline.settings?.protocolDetection?.enabled;
65 expect(baselineActivityLog).toEqual(true);
66 expect(baselineProtocolDetection).toEqual(false);
67
68 // Drive both toggles to the opposite of the baseline so the deploy
69 // is guaranteed to change something.
70 const flippedActivityLog = false;
71 const flippedProtocolDetection = true;
72
73 const config = yield* stack.deploy(
74 Cloudflare.Gateway.Configuration("Gateway", {
75 settings: {
76 activityLog: { enabled: flippedActivityLog },
77 protocolDetection: { enabled: flippedProtocolDetection },
78 },
79 }),
80 );
81 expect(config.accountId).toEqual(accountId);
82 // The pre-management blocks were captured for restore.
83 expect(Object.keys(config.initialSettings).sort()).toEqual([
84 "activityLog",
85 "protocolDetection",
86 ]);
87
88 const live = yield* getConfiguration(accountId);
89 expect(live.settings?.activityLog?.enabled).toEqual(flippedActivityLog);
90 expect(live.settings?.protocolDetection?.enabled).toEqual(
91 flippedProtocolDetection,
92 );
93
94 // Update in place — flip activityLog back; protocolDetection stays.
95 const updated = yield* stack.deploy(
96 Cloudflare.Gateway.Configuration("Gateway", {
97 settings: {
98 activityLog: { enabled: !flippedActivityLog },
99 protocolDetection: { enabled: flippedProtocolDetection },
100 },
101 }),
102 );
103 // The original capture survives the update.
104 expect(updated.initialSettings).toEqual(config.initialSettings);
105
106 const liveAfter = yield* getConfiguration(accountId);
107 expect(liveAfter.settings?.activityLog?.enabled).toEqual(
108 !flippedActivityLog,
109 );
110
111 // Destroy restores the captured pre-management values.
112 yield* stack.destroy();
113 const restored = yield* getConfiguration(accountId);
114 expect(restored.settings?.activityLog?.enabled).toEqual(
115 baselineActivityLog,
116 );
117 expect(restored.settings?.protocolDetection?.enabled).toEqual(
118 baselineProtocolDetection,
119 );
120 }).pipe(logLevel),
121 {
122 tags: ["provider:cloudflare", "provider:cloudflare:gateway", "live"],
123 timeout: 120_000,
124 },
125);
126
127test.provider(
128 "list returns the account Gateway configuration singleton",
129 (stack) =>
130 Effect.gen(function* () {
131 yield* stack.destroy();
132
133 const { accountId } = yield* yield* CloudflareEnvironment;
134
135 const provider = yield* Provider.findProvider(
136 Cloudflare.Gateway.Configuration,
137 );
138 const all = yield* provider.list();
139
140 // Account-wide singleton: exactly one element for the ambient account.
141 expect(all.length).toEqual(1);
142 expect(all[0]!.accountId).toEqual(accountId);
143 expect(all[0]!.initialSettings).toEqual({});
144
145 yield* stack.destroy();
146 }).pipe(logLevel),
147 {
148 tags: ["provider:cloudflare", "provider:cloudflare:gateway", "live"],
149 timeout: 120_000,
150 },
151);