1import { adopt } from "@/AdoptPolicy";
2import * as Cloudflare from "@/Cloudflare";
3import { CloudflareEnvironment } from "@/Cloudflare/CloudflareEnvironment";
4import * as Provider from "@/Provider";
5import * as Test from "@/Test/Alchemy";
6import * as emailSecurity from "@distilled.cloud/cloudflare/email-security";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { MinimumLogLevel } from "effect/References";
10import * as Schedule from "effect/Schedule";
11import * as Stream from "effect/Stream";
12
13const { test } = Test.make({ providers: Cloudflare.providers() });
14
15const logLevel = Effect.provideService(
16 MinimumLogLevel,
17 process.env.DEBUG ? "Debug" : "Info",
18);
19
20// Email Security (Area 1) is an enterprise add-on AND domains can only be
21// onboarded in the dashboard. The lifecycle test needs an entitled account
22// plus an explicitly sacrificial onboarded domain named via env —
23// **destroying the resource offboards the domain from Email Security**, so
24// never point this at a production domain.
25const sacrificialDomain = process.env.CLOUDFLARE_EMAIL_SECURITY_DOMAIN;
26
27const forbiddenRetrySchedule = Schedule.exponential("500 millis");
28
29const findDomain = (accountId: string, domain: string) =>
30 emailSecurity.listSettingDomains.items({ accountId, domain: [domain] }).pipe(
31 Stream.runCollect,
32 Effect.map((chunk) => Array.from(chunk).find((d) => d.domain === domain)),
33 Effect.retry({
34 while: (e) => e._tag === "Forbidden",
35 schedule: forbiddenRetrySchedule,
36 times: 8,
37 }),
38 );
39
40test.provider.skipIf(!sacrificialDomain)(
41 "adopts an onboarded domain, patches settings in place, offboards on destroy",
42 (stack) =>
43 Effect.gen(function* () {
44 const { accountId } = yield* yield* CloudflareEnvironment;
45 const domainName = sacrificialDomain!;
46
47 const baseline = yield* findDomain(accountId, domainName);
48 expect(baseline).toBeDefined();
49
50 yield* stack.destroy();
51
52 // Adopt the onboarded domain — `read` reports it Unowned, so taking
53 // it under management requires `adopt(true)`.
54 const created = yield* stack.deploy(
55 Effect.gen(function* () {
56 return yield* Cloudflare.Email.Domain("MailDomain", {
57 domain: domainName,
58 lookbackHops: 5,
59 }).pipe(adopt(true));
60 }),
61 );
62 expect(created.domainId).toEqual(baseline?.id);
63 expect(created.domain).toEqual(domainName);
64 expect(created.lookbackHops).toEqual(5);
65
66 // Update settings in place — same physical domain.
67 const updated = yield* stack.deploy(
68 Effect.gen(function* () {
69 return yield* Cloudflare.Email.Domain("MailDomain", {
70 domain: domainName,
71 lookbackHops: 10,
72 dropDispositions: ["MALICIOUS"],
73 }).pipe(adopt(true));
74 }),
75 );
76 expect(updated.domainId).toEqual(created.domainId);
77 expect(updated.lookbackHops).toEqual(10);
78 expect(updated.dropDispositions).toEqual(["MALICIOUS"]);
79
80 // Out-of-band verification via the distilled API.
81 const live = yield* findDomain(accountId, domainName);
82 expect(live?.lookbackHops).toEqual(10);
83
84 // DESTRUCTIVE: destroy offboards the domain from Email Security.
85 yield* stack.destroy();
86
87 const gone = yield* findDomain(accountId, domainName).pipe(
88 Effect.repeat({
89 schedule: Schedule.spaced("2 seconds"),
90 until: (d) => d === undefined,
91 times: 10,
92 }),
93 );
94 expect(gone).toBeUndefined();
95 }).pipe(logLevel),
96 {
97 tags: ["provider:cloudflare", "provider:cloudflare:email", "live"],
98 timeout: 120_000,
99 },
100);
101
102// Read-only list assertion — safe to run on any account. Email Security is
103// a paid add-on; on accounts without the entitlement the account-scoped
104// list op rejects with EmailSecurityNotEntitled / Forbidden, which the
105// provider's `list()` maps to a well-typed []. On entitled accounts every
106// onboarded domain is returned in the exact `read` Attributes shape.
107test.provider(
108 "list enumerates Email Security domains (or [] when unentitled)",
109 (stack) =>
110 Effect.gen(function* () {
111 yield* stack.destroy();
112
113 const provider = yield* Provider.findProvider(Cloudflare.Email.Domain);
114 const all = yield* provider.list();
115 expect(Array.isArray(all)).toBe(true);
116
117 // When an entitled sacrificial domain is configured, it must appear in
118 // the exhaustively-paginated result in the read Attributes shape.
119 if (sacrificialDomain) {
120 const { accountId } = yield* yield* CloudflareEnvironment;
121 const baseline = yield* findDomain(accountId, sacrificialDomain);
122 if (baseline) {
123 expect(all.some((d) => d.domainId === baseline.id)).toBe(true);
124 }
125 }
126
127 yield* stack.destroy();
128 }).pipe(logLevel),
129 {
130 tags: ["provider:cloudflare", "provider:cloudflare:email", "live"],
131 timeout: 120_000,
132 },
133);