1import * as Cloudflare from "@/Cloudflare";
2import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
3import { findZoneByName } from "@/Cloudflare/Zone/lookup";
4import * as Provider from "@/Provider";
5import * as Test from "@/Test/Alchemy";
6import * as emailRouting from "@distilled.cloud/cloudflare/email-routing";
7import { describe, expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { MinimumLogLevel } from "effect/References";
10import * as Schedule from "effect/Schedule";
11import { emailRoutingScoped } from "./scope.ts";
12const { test } = Test.make({ providers: Cloudflare.providers() });
13
14const logLevel = Effect.provideService(
15 MinimumLogLevel,
16 process.env.DEBUG ? "Debug" : "Info",
17);
18
19const zoneName =
20 process.env.CLOUDFLARE_TEST_DNS_ZONE_NAME ?? "alchemy-test-2.us";
21
22const resolveZoneId = Effect.gen(function* () {
23 const { accountId } = yield* yield* CloudflareEnvironment;
24 const zone = yield* findZoneByName({ accountId, name: zoneName });
25 if (!zone) {
26 return yield* Effect.die(
27 new Error(`zone "${zoneName}" not found in account`),
28 );
29 }
30 return zone.id;
31});
32
33// The scoped API token the test harness mints propagates eventually-
34// consistently across Cloudflare's edge — a fresh token intermittently 403s
35// with "Unable to authenticate request". Ride out the blips on the test's
36// own out-of-band calls by retrying the typed `Forbidden` error (part of
37// the email-routing enable operation's error union via distilled patches).
38const forbiddenRetrySchedule = Schedule.exponential("500 millis");
39
40// Email Routing must be enabled on the zone for rules to be created and
41// visible to `list()`.
42const enableRouting = (zoneId: string) =>
43 emailRouting.enableEmailRouting({ zoneId }).pipe(
44 Effect.retry({
45 while: (e) => e._tag === "Forbidden",
46 schedule: forbiddenRetrySchedule,
47 times: 8,
48 }),
49 );
50
51describe.sequential.skipIf(!emailRoutingScoped)(
52 "EmailRule",
53 {
54 tags: [
55 "provider:cloudflare",
56 "provider:cloudflare:email",
57 "provider:cloudflare:zone",
58 "live",
59 ],
60 },
61 () => {
62 // Canonical `list()` test (zone-scoped collection): email routing rules live
63 // under `/zones/{id}/email/routing/rules` with no account-wide enumeration
64 // API, so `list()` enumerates every zone via `listAllZones` and exhaustively
65 // paginates each zone's rules (skipping zones without Email Routing enabled).
66 // Deploy a rule on the standing test zone, then assert it appears in the
67 // exhaustively-paginated result.
68 test.provider(
69 "list enumerates the deployed email rule across all zones",
70 (stack) =>
71 Effect.gen(function* () {
72 const zoneId = yield* resolveZoneId;
73
74 yield* stack.destroy();
75 yield* enableRouting(zoneId);
76
77 const rule = yield* stack.deploy(
78 Effect.gen(function* () {
79 const routing = yield* Cloudflare.Email.Routing("Routing", {
80 zone: zoneName,
81 });
82 return yield* Cloudflare.Email.Rule("ListRule", {
83 zone: { zoneId: routing.zoneId },
84 name: "alchemy list test",
85 matchers: [
86 {
87 type: "literal",
88 field: "to",
89 value: "list@alchemy-test-2.us",
90 },
91 ],
92 actions: [{ type: "drop" }],
93 });
94 }),
95 );
96
97 expect(rule.zoneId).toEqual(zoneId);
98 expect(rule.ruleId).not.toEqual("");
99
100 const provider = yield* Provider.findProvider(Cloudflare.Email.Rule);
101 // The freshly-minted scoped token propagates eventually-consistently,
102 // so the account-wide enumeration intermittently 401s (`Unauthorized`,
103 // code 10000) or 403s (`Forbidden`). Both are transient here — ride
104 // out the blip like every other out-of-band call in this suite.
105 const all = yield* provider.list().pipe(
106 Effect.retry({
107 while: (e) => e._tag === "Forbidden" || e._tag === "Unauthorized",
108 schedule: forbiddenRetrySchedule,
109 times: 8,
110 }),
111 );
112
113 const row = all.find((r) => r.ruleId === rule.ruleId);
114 expect(row).toBeDefined();
115 expect(row!.zoneId).toEqual(zoneId);
116 expect(row!.name).toEqual("alchemy list test");
117 expect(Array.isArray(row!.matchers)).toBe(true);
118 expect(row!.actions).toEqual([{ type: "drop" }]);
119
120 yield* stack.destroy();
121 }).pipe(logLevel),
122 );
123 },
124);