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 { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9const { test } = Test.make({ providers: Cloudflare.providers() });
10
11const logLevel = Effect.provideService(
12 MinimumLogLevel,
13 process.env.DEBUG ? "Debug" : "Info",
14);
15
16const AUTH_DOMAIN = process.env.CLOUDFLARE_TEST_AUTH_DOMAIN;
17const skip = !AUTH_DOMAIN;
18
19// The Access organization is an account-wide singleton whose auth domain is
20// allocated once and effectively immutable — the test adopts and mutates the
21// LIVE org, so it only runs when the operator opts in by setting
22// CLOUDFLARE_TEST_AUTH_DOMAIN to the account's <team>.cloudflareaccess.com domain.
23// Both cases mutate the same account-wide Access organization singleton; run
24// them serially so they don't corrupt each other under the global concurrent
25// test config.
26describe.sequential(
27 "Organization",
28 { tags: ["provider:cloudflare", "provider:cloudflare:access", "live"] },
29 () => {
30 test.provider.skipIf(skip)(
31 "adopts the existing Access organization",
32 (stack) =>
33 Effect.gen(function* () {
34 const { accountId } = yield* yield* CloudflareEnvironment;
35
36 const org = yield* stack.deploy(
37 Effect.gen(function* () {
38 return yield* Cloudflare.Access.Organization("Org", {
39 authDomain: AUTH_DOMAIN!,
40 name: AUTH_DOMAIN!,
41 });
42 }),
43 );
44
45 expect(org.accountId).toEqual(accountId);
46 expect(org.authDomain).toEqual(AUTH_DOMAIN);
47
48 const live = yield* zeroTrust.listOrganizationsForAccount({
49 accountId,
50 });
51 expect(live.authDomain).toEqual(AUTH_DOMAIN);
52
53 // Singleton: delete is a no-op, so destroy must NOT remove the org.
54 yield* stack.destroy();
55 const stillThere = yield* zeroTrust.listOrganizationsForAccount({
56 accountId,
57 });
58 expect(stillThere.authDomain).toEqual(AUTH_DOMAIN);
59 }).pipe(logLevel),
60 );
61
62 // Same opt-in gate as above: toggling allow_authenticate_via_warp mutates the
63 // account's live singleton org, so it requires CLOUDFLARE_TEST_AUTH_DOMAIN.
64 test.provider.skipIf(skip)(
65 "toggles allow_authenticate_via_warp and restores",
66 (stack) =>
67 Effect.gen(function* () {
68 const { accountId } = yield* yield* CloudflareEnvironment;
69
70 const original = yield* zeroTrust.listOrganizationsForAccount({
71 accountId,
72 });
73 const originalWarp = original.allowAuthenticateViaWarp ?? false;
74 const originalName = original.name ?? AUTH_DOMAIN!;
75
76 // Step 1 — set to true.
77 const enabled = yield* stack.deploy(
78 Effect.gen(function* () {
79 return yield* Cloudflare.Access.Organization("Org", {
80 authDomain: AUTH_DOMAIN!,
81 name: originalName,
82 allowAuthenticateViaWarp: true,
83 });
84 }),
85 );
86 expect(enabled.allowAuthenticateViaWarp).toEqual(true);
87 const liveEnabled = yield* zeroTrust.listOrganizationsForAccount({
88 accountId,
89 });
90 expect(liveEnabled.allowAuthenticateViaWarp).toEqual(true);
91
92 // Step 2 — flip to false.
93 const disabled = yield* stack.deploy(
94 Effect.gen(function* () {
95 return yield* Cloudflare.Access.Organization("Org", {
96 authDomain: AUTH_DOMAIN!,
97 name: originalName,
98 allowAuthenticateViaWarp: false,
99 });
100 }),
101 );
102 expect(disabled.allowAuthenticateViaWarp).toEqual(false);
103 const liveDisabled = yield* zeroTrust.listOrganizationsForAccount({
104 accountId,
105 });
106 expect(liveDisabled.allowAuthenticateViaWarp).toEqual(false);
107
108 // Restore to original value via a final deploy so the account
109 // isn't left mutated by the test.
110 yield* stack.deploy(
111 Effect.gen(function* () {
112 return yield* Cloudflare.Access.Organization("Org", {
113 authDomain: AUTH_DOMAIN!,
114 name: originalName,
115 allowAuthenticateViaWarp: originalWarp,
116 });
117 }),
118 );
119
120 yield* stack.destroy();
121 }).pipe(logLevel),
122 );
123
124 // Canonical `list()` test (account singleton): there is no enumeration API
125 // for the Access organization, so `list()` reads the single account-wide
126 // org via the same path `read` uses and returns the one-element array (or
127 // `[]` when the account has never enabled Zero Trust). This is read-only —
128 // it does not mutate the singleton — so it runs unconditionally.
129 test.provider("list returns the account Access organization", (stack) =>
130 Effect.gen(function* () {
131 const { accountId } = yield* yield* CloudflareEnvironment;
132
133 const provider = yield* Provider.findProvider(
134 Cloudflare.Access.Organization,
135 );
136 const all = yield* provider.list();
137
138 // Singleton: zero (Zero Trust never enabled) or exactly one.
139 expect(all.length).toBeLessThanOrEqual(1);
140 for (const org of all) {
141 expect(org.accountId).toEqual(accountId);
142 expect(typeof org.authDomain).toBe("string");
143 expect(typeof org.name).toBe("string");
144 }
145
146 // `stack` is unused (no resource is deployed), but keep the destroy
147 // bookend so the harness state stays clean.
148 yield* stack.destroy();
149 }).pipe(logLevel),
150 );
151 },
152);