@org-press/deploy-cloudflare 0.9.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,693 @@
1
+ /**
2
+ * Cloudflare Pages Adapter tests
3
+ */
4
+
5
+ import { describe, it, expect, vi, beforeEach } from "vitest";
6
+ import * as childProcess from "node:child_process";
7
+ import { CloudflareAdapter, cloudflareAdapter } from "./adapter.ts";
8
+ import type {
9
+ DeployContext,
10
+ AdapterConfig,
11
+ PackageMetadata,
12
+ DeployLogger,
13
+ } from "@org-press/deploy";
14
+
15
+ // Mock child_process
16
+ vi.mock("node:child_process", () => ({
17
+ spawnSync: vi.fn(),
18
+ }));
19
+
20
+ describe("CloudflareAdapter", () => {
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ // Reset process.env mock
24
+ vi.stubEnv("CF_ACCOUNT_ID", undefined);
25
+ vi.stubEnv("CLOUDFLARE_API_TOKEN", undefined);
26
+ vi.stubEnv("CF_API_TOKEN", undefined);
27
+ });
28
+
29
+ describe("constructor", () => {
30
+ it("should create adapter with config", () => {
31
+ const adapter = new CloudflareAdapter({ project: "my-site" });
32
+ expect(adapter.name).toBe("cloudflare");
33
+ expect(adapter.description).toBe("Deploy to Cloudflare Pages");
34
+ });
35
+
36
+ it("should accept full config", () => {
37
+ const adapter = new CloudflareAdapter({
38
+ project: "my-site",
39
+ accountId: "abc123",
40
+ branch: "preview",
41
+ commitMessage: "Custom deploy message",
42
+ });
43
+ expect(adapter.name).toBe("cloudflare");
44
+ });
45
+ });
46
+
47
+ describe("validate", () => {
48
+ it("should validate successfully when wrangler is available", async () => {
49
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
50
+ status: 0,
51
+ stdout: "wrangler 3.0.0",
52
+ stderr: "",
53
+ pid: 123,
54
+ output: [],
55
+ signal: null,
56
+ });
57
+
58
+ const adapter = new CloudflareAdapter({ project: "my-site" });
59
+ const config: AdapterConfig = {
60
+ options: {},
61
+ env: { CLOUDFLARE_API_TOKEN: "test-token" },
62
+ };
63
+
64
+ const result = await adapter.validate(config);
65
+
66
+ expect(result.valid).toBe(true);
67
+ expect(result.errors).toEqual([]);
68
+ });
69
+
70
+ it("should fail if wrangler is not available", async () => {
71
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
72
+ status: 1,
73
+ stdout: "",
74
+ stderr: "command not found: wrangler",
75
+ pid: 123,
76
+ output: [],
77
+ signal: null,
78
+ });
79
+
80
+ const adapter = new CloudflareAdapter({ project: "my-site" });
81
+ const config: AdapterConfig = {
82
+ options: {},
83
+ env: {},
84
+ };
85
+
86
+ const result = await adapter.validate(config);
87
+
88
+ expect(result.valid).toBe(false);
89
+ expect(result.errors).toContain(
90
+ "wrangler is not available. Install with: npm install -D wrangler"
91
+ );
92
+ });
93
+
94
+ it("should fail if project name is missing", async () => {
95
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
96
+ status: 0,
97
+ stdout: "wrangler 3.0.0",
98
+ stderr: "",
99
+ pid: 123,
100
+ output: [],
101
+ signal: null,
102
+ });
103
+
104
+ // @ts-expect-error Testing missing required field
105
+ const adapter = new CloudflareAdapter({});
106
+ const config: AdapterConfig = {
107
+ options: {},
108
+ env: {},
109
+ };
110
+
111
+ const result = await adapter.validate(config);
112
+
113
+ expect(result.valid).toBe(false);
114
+ expect(result.errors).toContain("Cloudflare Pages project name is required");
115
+ });
116
+
117
+ it("should fail with invalid project name format", async () => {
118
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
119
+ status: 0,
120
+ stdout: "wrangler 3.0.0",
121
+ stderr: "",
122
+ pid: 123,
123
+ output: [],
124
+ signal: null,
125
+ });
126
+
127
+ const adapter = new CloudflareAdapter({ project: "Invalid_Project!" });
128
+ const config: AdapterConfig = {
129
+ options: {},
130
+ env: {},
131
+ };
132
+
133
+ const result = await adapter.validate(config);
134
+
135
+ expect(result.valid).toBe(false);
136
+ expect(result.errors.some((e) => e.includes("Invalid project name"))).toBe(
137
+ true
138
+ );
139
+ });
140
+
141
+ it("should accept valid project names", async () => {
142
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
143
+ status: 0,
144
+ stdout: "wrangler 3.0.0",
145
+ stderr: "",
146
+ pid: 123,
147
+ output: [],
148
+ signal: null,
149
+ });
150
+
151
+ const validNames = ["mysite", "my-site", "site123", "a", "my-cool-site-2"];
152
+
153
+ for (const project of validNames) {
154
+ const adapter = new CloudflareAdapter({ project });
155
+ const config: AdapterConfig = {
156
+ options: {},
157
+ env: { CLOUDFLARE_API_TOKEN: "token" },
158
+ };
159
+
160
+ const result = await adapter.validate(config);
161
+ expect(result.valid).toBe(true);
162
+ expect(result.errors).toEqual([]);
163
+ }
164
+ });
165
+
166
+ it("should reject invalid project names", async () => {
167
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
168
+ status: 0,
169
+ stdout: "wrangler 3.0.0",
170
+ stderr: "",
171
+ pid: 123,
172
+ output: [],
173
+ signal: null,
174
+ });
175
+
176
+ const invalidNames = ["-mysite", "mysite-", "My_Site", "site!"];
177
+
178
+ for (const project of invalidNames) {
179
+ const adapter = new CloudflareAdapter({ project });
180
+ const config: AdapterConfig = {
181
+ options: {},
182
+ env: {},
183
+ };
184
+
185
+ const result = await adapter.validate(config);
186
+ expect(result.errors.some((e) => e.includes("Invalid project name"))).toBe(
187
+ true
188
+ );
189
+ }
190
+ });
191
+
192
+ it("should warn when no API token is found", async () => {
193
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
194
+ status: 0,
195
+ stdout: "wrangler 3.0.0",
196
+ stderr: "",
197
+ pid: 123,
198
+ output: [],
199
+ signal: null,
200
+ });
201
+
202
+ const adapter = new CloudflareAdapter({ project: "my-site" });
203
+ const config: AdapterConfig = {
204
+ options: {},
205
+ env: {},
206
+ };
207
+
208
+ const result = await adapter.validate(config);
209
+
210
+ expect(result.valid).toBe(true);
211
+ expect(result.warnings.some((w) => w.includes("API_TOKEN"))).toBe(true);
212
+ });
213
+
214
+ it("should not warn when CLOUDFLARE_API_TOKEN is set", async () => {
215
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
216
+ status: 0,
217
+ stdout: "wrangler 3.0.0",
218
+ stderr: "",
219
+ pid: 123,
220
+ output: [],
221
+ signal: null,
222
+ });
223
+
224
+ const adapter = new CloudflareAdapter({ project: "my-site" });
225
+ const config: AdapterConfig = {
226
+ options: {},
227
+ env: { CLOUDFLARE_API_TOKEN: "test-token" },
228
+ };
229
+
230
+ const result = await adapter.validate(config);
231
+
232
+ expect(result.valid).toBe(true);
233
+ expect(result.warnings.some((w) => w.includes("API_TOKEN"))).toBe(false);
234
+ });
235
+
236
+ it("should not warn when CF_API_TOKEN is set", async () => {
237
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
238
+ status: 0,
239
+ stdout: "wrangler 3.0.0",
240
+ stderr: "",
241
+ pid: 123,
242
+ output: [],
243
+ signal: null,
244
+ });
245
+
246
+ const adapter = new CloudflareAdapter({ project: "my-site" });
247
+ const config: AdapterConfig = {
248
+ options: {},
249
+ env: { CF_API_TOKEN: "test-token" },
250
+ };
251
+
252
+ const result = await adapter.validate(config);
253
+
254
+ expect(result.valid).toBe(true);
255
+ expect(result.warnings.some((w) => w.includes("API_TOKEN"))).toBe(false);
256
+ });
257
+
258
+ it("should warn when no account ID is found", async () => {
259
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
260
+ status: 0,
261
+ stdout: "wrangler 3.0.0",
262
+ stderr: "",
263
+ pid: 123,
264
+ output: [],
265
+ signal: null,
266
+ });
267
+
268
+ const adapter = new CloudflareAdapter({ project: "my-site" });
269
+ const config: AdapterConfig = {
270
+ options: {},
271
+ env: { CLOUDFLARE_API_TOKEN: "token" },
272
+ };
273
+
274
+ const result = await adapter.validate(config);
275
+
276
+ expect(result.valid).toBe(true);
277
+ expect(result.warnings.some((w) => w.includes("account ID"))).toBe(true);
278
+ });
279
+
280
+ it("should use options over constructor config for validation", async () => {
281
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
282
+ status: 0,
283
+ stdout: "wrangler 3.0.0",
284
+ stderr: "",
285
+ pid: 123,
286
+ output: [],
287
+ signal: null,
288
+ });
289
+
290
+ const adapter = new CloudflareAdapter({ project: "constructor-project" });
291
+ const config: AdapterConfig = {
292
+ options: { project: "options-project" },
293
+ env: { CLOUDFLARE_API_TOKEN: "token", CF_ACCOUNT_ID: "account" },
294
+ };
295
+
296
+ const result = await adapter.validate(config);
297
+
298
+ expect(result.valid).toBe(true);
299
+ });
300
+ });
301
+
302
+ describe("deploy", () => {
303
+ const createLogger = (): DeployLogger & { logs: string[] } => {
304
+ const logs: string[] = [];
305
+ return {
306
+ logs,
307
+ info: (msg) => logs.push(`INFO: ${msg}`),
308
+ warn: (msg) => logs.push(`WARN: ${msg}`),
309
+ error: (msg) => logs.push(`ERROR: ${msg}`),
310
+ debug: (msg) => logs.push(`DEBUG: ${msg}`),
311
+ };
312
+ };
313
+
314
+ const createContext = (
315
+ overrides: Partial<DeployContext> = {}
316
+ ): DeployContext => {
317
+ const metadata: PackageMetadata = {
318
+ name: "test-site",
319
+ version: "1.0.0",
320
+ description: "Test site",
321
+ };
322
+
323
+ return {
324
+ outDir: "/tmp/test-deploy",
325
+ metadata,
326
+ adapterConfig: {},
327
+ environment: "production",
328
+ dryRun: false,
329
+ logger: createLogger(),
330
+ ...overrides,
331
+ };
332
+ };
333
+
334
+ it("should return success for dry run without executing wrangler", async () => {
335
+ const adapter = new CloudflareAdapter({ project: "my-site" });
336
+ const context = createContext({ dryRun: true });
337
+
338
+ const result = await adapter.deploy(context);
339
+
340
+ expect(result.success).toBe(true);
341
+ expect(result.deploymentId).toContain("dry-run-");
342
+ expect(result.url).toBe("https://my-site.pages.dev");
343
+ expect(childProcess.spawnSync).not.toHaveBeenCalled();
344
+ });
345
+
346
+ it("should return branch URL for dry run with branch", async () => {
347
+ const adapter = new CloudflareAdapter({
348
+ project: "my-site",
349
+ branch: "preview",
350
+ });
351
+ const context = createContext({ dryRun: true });
352
+
353
+ const result = await adapter.deploy(context);
354
+
355
+ expect(result.success).toBe(true);
356
+ expect(result.url).toBe("https://preview.my-site.pages.dev");
357
+ });
358
+
359
+ it("should call wrangler with correct arguments", async () => {
360
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
361
+ status: 0,
362
+ stdout: "Published to https://my-site.pages.dev",
363
+ stderr: "",
364
+ pid: 123,
365
+ output: [],
366
+ signal: null,
367
+ });
368
+
369
+ const adapter = new CloudflareAdapter({ project: "my-site" });
370
+ const context = createContext();
371
+
372
+ await adapter.deploy(context);
373
+
374
+ expect(childProcess.spawnSync).toHaveBeenCalledWith(
375
+ "npx",
376
+ expect.arrayContaining([
377
+ "wrangler",
378
+ "pages",
379
+ "deploy",
380
+ "/tmp/test-deploy",
381
+ "--project-name",
382
+ "my-site",
383
+ "--commit-message",
384
+ "Deploy from org-press",
385
+ ]),
386
+ expect.objectContaining({
387
+ encoding: "utf-8",
388
+ timeout: 300000,
389
+ })
390
+ );
391
+ });
392
+
393
+ it("should include branch argument for branch deployments", async () => {
394
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
395
+ status: 0,
396
+ stdout: "Published to https://preview.my-site.pages.dev",
397
+ stderr: "",
398
+ pid: 123,
399
+ output: [],
400
+ signal: null,
401
+ });
402
+
403
+ const adapter = new CloudflareAdapter({
404
+ project: "my-site",
405
+ branch: "preview",
406
+ });
407
+ const context = createContext();
408
+
409
+ await adapter.deploy(context);
410
+
411
+ expect(childProcess.spawnSync).toHaveBeenCalledWith(
412
+ "npx",
413
+ expect.arrayContaining(["--branch", "preview"]),
414
+ expect.any(Object)
415
+ );
416
+ });
417
+
418
+ it("should use custom commit message", async () => {
419
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
420
+ status: 0,
421
+ stdout: "Published to https://my-site.pages.dev",
422
+ stderr: "",
423
+ pid: 123,
424
+ output: [],
425
+ signal: null,
426
+ });
427
+
428
+ const adapter = new CloudflareAdapter({
429
+ project: "my-site",
430
+ commitMessage: "Custom deploy message",
431
+ });
432
+ const context = createContext();
433
+
434
+ await adapter.deploy(context);
435
+
436
+ expect(childProcess.spawnSync).toHaveBeenCalledWith(
437
+ "npx",
438
+ expect.arrayContaining(["--commit-message", "Custom deploy message"]),
439
+ expect.any(Object)
440
+ );
441
+ });
442
+
443
+ it("should use adapterConfig options over constructor config", async () => {
444
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
445
+ status: 0,
446
+ stdout: "Published to https://custom-project.pages.dev",
447
+ stderr: "",
448
+ pid: 123,
449
+ output: [],
450
+ signal: null,
451
+ });
452
+
453
+ const adapter = new CloudflareAdapter({
454
+ project: "default-project",
455
+ branch: "default-branch",
456
+ commitMessage: "Default message",
457
+ });
458
+
459
+ const context = createContext({
460
+ adapterConfig: {
461
+ project: "custom-project",
462
+ branch: "custom-branch",
463
+ commitMessage: "Custom message",
464
+ },
465
+ });
466
+
467
+ await adapter.deploy(context);
468
+
469
+ expect(childProcess.spawnSync).toHaveBeenCalledWith(
470
+ "npx",
471
+ expect.arrayContaining([
472
+ "--project-name",
473
+ "custom-project",
474
+ "--branch",
475
+ "custom-branch",
476
+ "--commit-message",
477
+ "Custom message",
478
+ ]),
479
+ expect.any(Object)
480
+ );
481
+ });
482
+
483
+ it("should return failure when wrangler fails", async () => {
484
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
485
+ status: 1,
486
+ stdout: "",
487
+ stderr: "Authentication failed",
488
+ pid: 123,
489
+ output: [],
490
+ signal: null,
491
+ });
492
+
493
+ const adapter = new CloudflareAdapter({ project: "my-site" });
494
+ const context = createContext();
495
+
496
+ const result = await adapter.deploy(context);
497
+
498
+ expect(result.success).toBe(false);
499
+ expect(result.error).toContain("Wrangler deployment failed");
500
+ expect(result.error).toContain("Authentication failed");
501
+ });
502
+
503
+ it("should parse deployment URL from wrangler output", async () => {
504
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
505
+ status: 0,
506
+ stdout:
507
+ "Uploading... Success!\nPublished to https://abc123.my-site.pages.dev",
508
+ stderr: "",
509
+ pid: 123,
510
+ output: [],
511
+ signal: null,
512
+ });
513
+
514
+ const adapter = new CloudflareAdapter({ project: "my-site" });
515
+ const context = createContext();
516
+
517
+ const result = await adapter.deploy(context);
518
+
519
+ expect(result.success).toBe(true);
520
+ expect(result.url).toBe("https://abc123.my-site.pages.dev");
521
+ });
522
+
523
+ it("should fallback to constructed URL if not parsed", async () => {
524
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
525
+ status: 0,
526
+ stdout: "Deployment complete!",
527
+ stderr: "",
528
+ pid: 123,
529
+ output: [],
530
+ signal: null,
531
+ });
532
+
533
+ const adapter = new CloudflareAdapter({ project: "my-site" });
534
+ const context = createContext();
535
+
536
+ const result = await adapter.deploy(context);
537
+
538
+ expect(result.success).toBe(true);
539
+ expect(result.url).toBe("https://my-site.pages.dev");
540
+ });
541
+
542
+ it("should construct branch URL when branch is specified", async () => {
543
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
544
+ status: 0,
545
+ stdout: "Deployment complete!",
546
+ stderr: "",
547
+ pid: 123,
548
+ output: [],
549
+ signal: null,
550
+ });
551
+
552
+ const adapter = new CloudflareAdapter({
553
+ project: "my-site",
554
+ branch: "staging",
555
+ });
556
+ const context = createContext();
557
+
558
+ const result = await adapter.deploy(context);
559
+
560
+ expect(result.success).toBe(true);
561
+ expect(result.url).toBe("https://staging.my-site.pages.dev");
562
+ });
563
+
564
+ it("should include output in logs", async () => {
565
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
566
+ status: 0,
567
+ stdout: "Uploaded 100 files\nPublished to https://my-site.pages.dev",
568
+ stderr: "",
569
+ pid: 123,
570
+ output: [],
571
+ signal: null,
572
+ });
573
+
574
+ const adapter = new CloudflareAdapter({ project: "my-site" });
575
+ const context = createContext();
576
+
577
+ const result = await adapter.deploy(context);
578
+
579
+ expect(result.success).toBe(true);
580
+ expect(result.logs).toContain(
581
+ "Uploaded 100 files\nPublished to https://my-site.pages.dev"
582
+ );
583
+ });
584
+
585
+ it("should set CLOUDFLARE_ACCOUNT_ID in environment when accountId is provided", async () => {
586
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
587
+ status: 0,
588
+ stdout: "Published to https://my-site.pages.dev",
589
+ stderr: "",
590
+ pid: 123,
591
+ output: [],
592
+ signal: null,
593
+ });
594
+
595
+ const adapter = new CloudflareAdapter({
596
+ project: "my-site",
597
+ accountId: "test-account-123",
598
+ });
599
+ const context = createContext();
600
+
601
+ await adapter.deploy(context);
602
+
603
+ expect(childProcess.spawnSync).toHaveBeenCalledWith(
604
+ "npx",
605
+ expect.any(Array),
606
+ expect.objectContaining({
607
+ env: expect.objectContaining({
608
+ CLOUDFLARE_ACCOUNT_ID: "test-account-123",
609
+ }),
610
+ })
611
+ );
612
+ });
613
+
614
+ it("should handle exceptions during deployment", async () => {
615
+ vi.mocked(childProcess.spawnSync).mockImplementation(() => {
616
+ throw new Error("Unexpected error");
617
+ });
618
+
619
+ const adapter = new CloudflareAdapter({ project: "my-site" });
620
+ const context = createContext();
621
+
622
+ const result = await adapter.deploy(context);
623
+
624
+ expect(result.success).toBe(false);
625
+ expect(result.error).toContain("Unexpected error");
626
+ });
627
+
628
+ it("should log deployment information", async () => {
629
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
630
+ status: 0,
631
+ stdout: "Published to https://my-site.pages.dev",
632
+ stderr: "",
633
+ pid: 123,
634
+ output: [],
635
+ signal: null,
636
+ });
637
+
638
+ const adapter = new CloudflareAdapter({
639
+ project: "my-site",
640
+ branch: "preview",
641
+ });
642
+ const logger = createLogger();
643
+ const context = createContext({ logger });
644
+
645
+ await adapter.deploy(context);
646
+
647
+ expect(logger.logs.some((l) => l.includes("my-site"))).toBe(true);
648
+ expect(logger.logs.some((l) => l.includes("Branch deployment"))).toBe(
649
+ true
650
+ );
651
+ expect(logger.logs.some((l) => l.includes("preview"))).toBe(true);
652
+ });
653
+
654
+ it("should log production deployment when no branch specified", async () => {
655
+ vi.mocked(childProcess.spawnSync).mockReturnValue({
656
+ status: 0,
657
+ stdout: "Published to https://my-site.pages.dev",
658
+ stderr: "",
659
+ pid: 123,
660
+ output: [],
661
+ signal: null,
662
+ });
663
+
664
+ const adapter = new CloudflareAdapter({ project: "my-site" });
665
+ const logger = createLogger();
666
+ const context = createContext({ logger });
667
+
668
+ await adapter.deploy(context);
669
+
670
+ expect(logger.logs.some((l) => l.includes("Production deployment"))).toBe(
671
+ true
672
+ );
673
+ });
674
+ });
675
+ });
676
+
677
+ describe("cloudflareAdapter factory", () => {
678
+ it("should create CloudflareAdapter instance", () => {
679
+ const adapter = cloudflareAdapter({ project: "my-site" });
680
+ expect(adapter).toBeInstanceOf(CloudflareAdapter);
681
+ expect(adapter.name).toBe("cloudflare");
682
+ });
683
+
684
+ it("should pass config to adapter", () => {
685
+ const adapter = cloudflareAdapter({
686
+ project: "my-site",
687
+ branch: "preview",
688
+ accountId: "acc123",
689
+ commitMessage: "Deploy!",
690
+ });
691
+ expect(adapter).toBeInstanceOf(CloudflareAdapter);
692
+ });
693
+ });