1import * as Cloudflare from "@/Cloudflare";
2import * as Provider from "@/Provider";
3import * as Test from "@/Test/Alchemy";
4import * as ddos from "@distilled.cloud/cloudflare/ddos-protection";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8
9const { test } = Test.make({ providers: Cloudflare.providers() });
10
11const logLevel = Effect.provideService(
12 MinimumLogLevel,
13 process.env.DEBUG ? "Debug" : "Info",
14);
15
16// Advanced TCP Protection is a Magic Transit (Enterprise add-on)
17// entitlement that the testing account does not have — every API call fails
18// with the typed `AdvancedTcpProtectionNotEntitled` error (Cloudflare code
19// 8888; asserted in AllowlistEntry.test.ts). The lifecycle suite is gated
20// behind an opt-in env var for entitled accounts.
21const magicTransit = process.env.CLOUDFLARE_TEST_MAGIC_TRANSIT;
22
23const accountId = Effect.gen(function* () {
24 const { accountId } = yield* yield* Cloudflare.CloudflareEnvironment;
25 return accountId;
26});
27
28test.provider.skipIf(!magicTransit)(
29 "creates a global SYN protection rule, updates it in place, and destroys",
30 (stack) =>
31 Effect.gen(function* () {
32 const acct = yield* accountId;
33
34 yield* stack.destroy();
35
36 // Create.
37 const rule = yield* stack.deploy(
38 Effect.gen(function* () {
39 return yield* Cloudflare.DdosProtection.SynProtectionRule("Rule", {
40 scope: "global",
41 mode: "monitoring",
42 burstSensitivity: "medium",
43 rateSensitivity: "medium",
44 });
45 }),
46 );
47 expect(rule.scope).toEqual("global");
48 expect(rule.name).toEqual("global");
49 expect(rule.mode).toEqual("monitoring");
50
51 // Out-of-band verification via the distilled API.
52 const live = yield* ddos.getAdvancedTcpProtectionSynProtectionRuleItem({
53 accountId: acct,
54 ruleId: rule.ruleId,
55 });
56 expect(live.mode).toEqual("monitoring");
57 expect(live.burstSensitivity).toEqual("medium");
58
59 // In-place update — mode and sensitivities are patched, id is stable.
60 const updated = yield* stack.deploy(
61 Effect.gen(function* () {
62 return yield* Cloudflare.DdosProtection.SynProtectionRule("Rule", {
63 scope: "global",
64 mode: "disabled",
65 burstSensitivity: "high",
66 rateSensitivity: "low",
67 });
68 }),
69 );
70 expect(updated.ruleId).toEqual(rule.ruleId);
71 expect(updated.mode).toEqual("disabled");
72 expect(updated.burstSensitivity).toEqual("high");
73 expect(updated.rateSensitivity).toEqual("low");
74
75 yield* stack.destroy();
76
77 // Gone — the typed SynProtectionRuleNotFound error proves deletion.
78 const error = yield* ddos
79 .getAdvancedTcpProtectionSynProtectionRuleItem({
80 accountId: acct,
81 ruleId: rule.ruleId,
82 })
83 .pipe(Effect.flip);
84 expect(error._tag).toEqual("SynProtectionRuleNotFound");
85 }).pipe(logLevel),
86 {
87 tags: ["provider:cloudflare", "provider:cloudflare:ddosprotection", "live"],
88 timeout: 120_000,
89 },
90);
91
92// Ungated: list() enumerates every rule in the ambient account. On the
93// unentitled testing account the typed `AdvancedTcpProtectionNotEntitled`
94// (Cloudflare code 8888) / `Forbidden` rejection is caught and surfaces as a
95// well-typed empty array — proving list() is resilient on accounts without
96// the Advanced TCP Protection entitlement.
97test.provider(
98 "list returns a well-typed array of SYN protection rules",
99 () =>
100 Effect.gen(function* () {
101 const provider = yield* Provider.findProvider(
102 Cloudflare.DdosProtection.SynProtectionRule,
103 );
104 const all = yield* provider.list();
105 expect(Array.isArray(all)).toBe(true);
106 for (const r of all) {
107 expect(typeof r.ruleId).toBe("string");
108 expect(typeof r.accountId).toBe("string");
109 }
110 }).pipe(logLevel),
111 {
112 tags: ["provider:cloudflare", "provider:cloudflare:ddosprotection", "live"],
113 timeout: 120_000,
114 },
115);
116
117// Gated full lifecycle: on an entitled account, a deployed rule must appear
118// in the exhaustively-paginated list().
119test.provider.skipIf(!magicTransit)(
120 "list enumerates the deployed SYN protection rule",
121 (stack) =>
122 Effect.gen(function* () {
123 yield* stack.destroy();
124
125 const rule = yield* stack.deploy(
126 Effect.gen(function* () {
127 return yield* Cloudflare.DdosProtection.SynProtectionRule(
128 "ListRule",
129 {
130 scope: "global",
131 mode: "monitoring",
132 burstSensitivity: "medium",
133 rateSensitivity: "medium",
134 },
135 );
136 }),
137 );
138
139 const provider = yield* Provider.findProvider(
140 Cloudflare.DdosProtection.SynProtectionRule,
141 );
142 const all = yield* provider.list();
143 expect(all.some((r) => r.ruleId === rule.ruleId)).toBe(true);
144
145 yield* stack.destroy();
146 }).pipe(logLevel),
147 {
148 tags: ["provider:cloudflare", "provider:cloudflare:ddosprotection", "live"],
149 timeout: 120_000,
150 },
151);