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 schemaValidation from "@distilled.cloud/cloudflare/schema-validation";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import * as FileSystem from "effect/FileSystem";
10import * as Path from "effect/Path";
11import { MinimumLogLevel } from "effect/References";
12import * as Schedule from "effect/Schedule";
13
14const { test } = Test.make({ providers: Cloudflare.providers() });
15
16const logLevel = Effect.provideService(
17 MinimumLogLevel,
18 process.env.DEBUG ? "Debug" : "Info",
19);
20
21const zoneName =
22 process.env.CLOUDFLARE_TEST_DNS_ZONE_NAME ?? "alchemy-test-2.us";
23
24const resolveZoneId = Effect.gen(function* () {
25 const { accountId } = yield* yield* CloudflareEnvironment;
26 const zone = yield* findZoneByName({ accountId, name: zoneName });
27 if (!zone) {
28 return yield* Effect.die(
29 new Error(`zone "${zoneName}" not found in account`),
30 );
31 }
32 return zone.id;
33});
34
35const readFixture = (name: string) =>
36 Effect.gen(function* () {
37 const fs = yield* FileSystem.FileSystem;
38 const path = yield* Path.Path;
39 return yield* fs.readFileString(
40 path.join(import.meta.dirname, "fixtures", name),
41 );
42 });
43
44// The scoped API token the test harness mints propagates eventually-
45// consistently across Cloudflare's edge — ride out 403 blips on the test's
46// own out-of-band calls by retrying the typed `Forbidden` error.
47const getSchemaOob = (zoneId: string, schemaId: string) =>
48 schemaValidation.getSchema({ zoneId, schemaId }).pipe(
49 Effect.retry({
50 while: (e) => e._tag === "Forbidden",
51 schedule: Schedule.exponential("500 millis"),
52 times: 8,
53 }),
54 );
55
56test.provider(
57 "uploads a schema, enables in place, replaces on disable and source change, destroys",
58 (stack) =>
59 Effect.gen(function* () {
60 const zoneId = yield* resolveZoneId;
61 const v1 = yield* readFixture("openapi-v1.json");
62 const v2 = yield* readFixture("openapi-v2.json");
63
64 yield* stack.destroy();
65
66 const created = yield* stack.deploy(
67 Effect.gen(function* () {
68 return yield* Cloudflare.SchemaValidation.SchemaValidationSchema(
69 "TestSchema",
70 {
71 zoneId,
72 source: v1,
73 validationEnabled: false,
74 },
75 );
76 }),
77 );
78
79 expect(created.schemaId).toBeTruthy();
80 expect(created.zoneId).toEqual(zoneId);
81 expect(created.kind).toEqual("openapi_v3");
82 expect(created.validationEnabled).toEqual(false);
83
84 const live = yield* getSchemaOob(zoneId, created.schemaId);
85 expect(live.name).toEqual(created.name);
86 expect(live.source).toEqual(v1);
87 expect(live.validationEnabled ?? false).toEqual(false);
88
89 // Enabling (false → true) is the one in-place update.
90 const enabled = yield* stack.deploy(
91 Effect.gen(function* () {
92 return yield* Cloudflare.SchemaValidation.SchemaValidationSchema(
93 "TestSchema",
94 {
95 zoneId,
96 source: v1,
97 validationEnabled: true,
98 },
99 );
100 }),
101 );
102 expect(enabled.schemaId).toEqual(created.schemaId);
103 expect(enabled.validationEnabled).toEqual(true);
104 expect(enabled.source).toEqual(v1);
105
106 const enabledLive = yield* getSchemaOob(zoneId, enabled.schemaId);
107 expect(enabledLive.validationEnabled).toEqual(true);
108
109 // Cloudflare rejects disabling in place — true → false replaces the
110 // schema (new id, old one deleted).
111 const disabled = yield* stack.deploy(
112 Effect.gen(function* () {
113 return yield* Cloudflare.SchemaValidation.SchemaValidationSchema(
114 "TestSchema",
115 {
116 zoneId,
117 source: v1,
118 validationEnabled: false,
119 },
120 );
121 }),
122 );
123 expect(disabled.schemaId).not.toEqual(created.schemaId);
124 expect(disabled.validationEnabled).toEqual(false);
125
126 const firstGone = yield* schemaValidation
127 .getSchema({ zoneId, schemaId: created.schemaId })
128 .pipe(Effect.flip);
129 expect(firstGone._tag).toEqual("SchemaNotFound");
130
131 // The schema body is immutable — changing the source uploads a new
132 // schema (new id) and deletes the old one.
133 const replaced = yield* stack.deploy(
134 Effect.gen(function* () {
135 return yield* Cloudflare.SchemaValidation.SchemaValidationSchema(
136 "TestSchema",
137 {
138 zoneId,
139 source: v2,
140 },
141 );
142 }),
143 );
144 expect(replaced.schemaId).not.toEqual(disabled.schemaId);
145 expect(replaced.source).toEqual(v2);
146
147 const oldGone = yield* schemaValidation
148 .getSchema({ zoneId, schemaId: disabled.schemaId })
149 .pipe(Effect.flip);
150 expect(oldGone._tag).toEqual("SchemaNotFound");
151
152 yield* stack.destroy();
153
154 const gone = yield* schemaValidation
155 .getSchema({ zoneId, schemaId: replaced.schemaId })
156 .pipe(Effect.flip);
157 expect(gone._tag).toEqual("SchemaNotFound");
158 }).pipe(logLevel),
159 {
160 tags: [
161 "provider:cloudflare",
162 "provider:cloudflare:schemavalidation",
163 "provider:cloudflare:zone",
164 "live",
165 ],
166 timeout: 120_000,
167 },
168);
169
170// Canonical `list()` test (zone-scoped collection): `list()` enumerates every
171// zone in the account via `listAllZones` and exhaustively paginates each
172// zone's schemas. Deploy a real schema and assert it appears in the result.
173test.provider(
174 "list enumerates the deployed schema across all zones",
175 (stack) =>
176 Effect.gen(function* () {
177 const zoneId = yield* resolveZoneId;
178 const v1 = yield* readFixture("openapi-v1.json");
179
180 yield* stack.destroy();
181
182 const deployed = yield* stack.deploy(
183 Effect.gen(function* () {
184 return yield* Cloudflare.SchemaValidation.SchemaValidationSchema(
185 "ListSchema",
186 {
187 zoneId,
188 source: v1,
189 validationEnabled: false,
190 },
191 );
192 }),
193 );
194
195 const provider = yield* Provider.findProvider(
196 Cloudflare.SchemaValidation.SchemaValidationSchema,
197 );
198 const all = yield* provider.list();
199
200 expect(
201 all.some(
202 (s) => s.schemaId === deployed.schemaId && s.zoneId === zoneId,
203 ),
204 ).toBe(true);
205
206 yield* stack.destroy();
207 }).pipe(logLevel),
208 {
209 tags: [
210 "provider:cloudflare",
211 "provider:cloudflare:schemavalidation",
212 "provider:cloudflare:zone",
213 "live",
214 ],
215 timeout: 120_000,
216 },
217);