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// The scoped API token the test harness mints propagates eventually-
19// consistently across Cloudflare's edge — ride out 403 blips
20// (`Forbidden`, declared in the distilled error union) on the test's
21// own out-of-band verification calls.
22const getProfile = (accountId: string, policyId: string) =>
23 zeroTrust.getDevicePolicyCustom({ accountId, policyId }).pipe(
24 Effect.retry({
25 while: (e) => e._tag === "Forbidden",
26 schedule: Schedule.exponential("500 millis"),
27 times: 8,
28 }),
29 );
30
31const expectGone = (accountId: string, policyId: string) =>
32 getProfile(accountId, policyId).pipe(
33 Effect.flatMap(() => Effect.fail({ _tag: "ProfileNotDeleted" } as const)),
34 // A missing profile surfaces as `DevicePolicyNotFound` (Cloudflare
35 // error code 2052) — that's the success condition here.
36 Effect.catchTag("DevicePolicyNotFound", () => Effect.void),
37 Effect.retry({
38 while: (e) => e._tag === "ProfileNotDeleted",
39 schedule: Schedule.max([
40 Schedule.exponential("500 millis"),
41 Schedule.recurs(10),
42 ]),
43 }),
44 );
45
46test.provider(
47 "create, update in place, and delete a custom device profile",
48 (stack) =>
49 Effect.gen(function* () {
50 const { accountId } = yield* yield* CloudflareEnvironment;
51
52 yield* stack.destroy();
53
54 const profile = yield* stack.deploy(
55 Cloudflare.Devices.DeviceCustomProfile("Contractors", {
56 name: "alchemy-test-custom-profile",
57 match: 'identity.email == "contractor@alchemy-test-2.us"',
58 precedence: 12010,
59 description: "Alchemy test profile",
60 switchLocked: true,
61 }),
62 );
63
64 expect(profile.policyId).toBeTruthy();
65 expect(profile.accountId).toEqual(accountId);
66 expect(profile.name).toEqual("alchemy-test-custom-profile");
67 expect(profile.match).toEqual(
68 'identity.email == "contractor@alchemy-test-2.us"',
69 );
70 expect(profile.description).toEqual("Alchemy test profile");
71 expect(profile.switchLocked).toEqual(true);
72 expect(profile.default).toEqual(false);
73
74 // Out-of-band verify against the live API.
75 const live = yield* getProfile(accountId, profile.policyId);
76 expect(live?.name).toEqual("alchemy-test-custom-profile");
77 expect(live?.switchLocked).toEqual(true);
78
79 // Update mutable props (body + split-tunnel exclude list) in place —
80 // same policyId.
81 const updated = yield* stack.deploy(
82 Cloudflare.Devices.DeviceCustomProfile("Contractors", {
83 name: "alchemy-test-custom-profile",
84 match: 'identity.email == "contractor@alchemy-test-2.us"',
85 precedence: 12010,
86 description: "Alchemy test profile v2",
87 switchLocked: false,
88 exclude: [{ address: "10.99.0.0/16", description: "test range" }],
89 }),
90 );
91 expect(updated.policyId).toEqual(profile.policyId);
92 expect(updated.description).toEqual("Alchemy test profile v2");
93 expect(updated.switchLocked).toEqual(false);
94 expect(updated.exclude.some((e) => e.address === "10.99.0.0/16")).toEqual(
95 true,
96 );
97
98 // Out-of-band verify the exclude list endpoint.
99 const excludes = yield* zeroTrust
100 .getDevicePolicyCustomExclude({
101 accountId,
102 policyId: profile.policyId,
103 })
104 .pipe(
105 Effect.retry({
106 while: (e) => e._tag === "Forbidden",
107 schedule: Schedule.exponential("500 millis"),
108 times: 8,
109 }),
110 );
111 expect(
112 (excludes.result ?? []).some(
113 (e) => "address" in e && e.address === "10.99.0.0/16",
114 ),
115 ).toEqual(true);
116
117 yield* stack.destroy();
118 yield* expectGone(accountId, profile.policyId);
119 }).pipe(logLevel),
120 { tags: ["provider:cloudflare", "provider:cloudflare:devices", "live"] },
121);
122
123test.provider(
124 "list enumerates the deployed custom device profile",
125 (stack) =>
126 Effect.gen(function* () {
127 yield* stack.destroy();
128
129 const deployed = yield* stack.deploy(
130 Cloudflare.Devices.DeviceCustomProfile("ListProfile", {
131 name: "alchemy-test-custom-profile-list",
132 match: 'identity.email == "list@alchemy-test-2.us"',
133 precedence: 12011,
134 description: "Alchemy list() test profile",
135 }),
136 );
137
138 const provider = yield* Provider.findProvider(
139 Cloudflare.Devices.DeviceCustomProfile,
140 );
141 const all = yield* provider.list();
142
143 // Exhaustively paginated account collection must contain the profile we
144 // just deployed, hydrated into the exact `read` Attributes shape.
145 const found = all.find((p) => p.policyId === deployed.policyId);
146 expect(found).toBeDefined();
147 expect(found?.name).toEqual("alchemy-test-custom-profile-list");
148 expect(found?.match).toEqual(
149 'identity.email == "list@alchemy-test-2.us"',
150 );
151 expect(found?.accountId).toEqual(deployed.accountId);
152 expect(found?.default).toEqual(false);
153
154 yield* stack.destroy();
155 }).pipe(logLevel),
156 { tags: ["provider:cloudflare", "provider:cloudflare:devices", "live"] },
157);