@jango-blockchained/hoox-cli 0.3.4

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.
Files changed (94) hide show
  1. package/README.md +403 -0
  2. package/bin/hoox.js +12 -0
  3. package/package.json +60 -0
  4. package/src/commands/check/check-command.test.ts +468 -0
  5. package/src/commands/check/check-command.ts +1144 -0
  6. package/src/commands/check/index.ts +10 -0
  7. package/src/commands/check/prerequisites-command.test.ts +19 -0
  8. package/src/commands/check/prerequisites-command.ts +92 -0
  9. package/src/commands/check/types.ts +103 -0
  10. package/src/commands/clone/clone-command.test.ts +442 -0
  11. package/src/commands/clone/clone-command.ts +440 -0
  12. package/src/commands/clone/index.ts +1 -0
  13. package/src/commands/config/config-command.test.ts +583 -0
  14. package/src/commands/config/config-command.ts +901 -0
  15. package/src/commands/config/env-command.test.ts +43 -0
  16. package/src/commands/config/env-command.ts +314 -0
  17. package/src/commands/config/index.ts +3 -0
  18. package/src/commands/config/kv-command.test.ts +14 -0
  19. package/src/commands/config/kv-command.ts +329 -0
  20. package/src/commands/dashboard/dashboard-command.test.ts +47 -0
  21. package/src/commands/dashboard/dashboard-command.ts +127 -0
  22. package/src/commands/dashboard/index.ts +1 -0
  23. package/src/commands/db/db-command.test.ts +21 -0
  24. package/src/commands/db/db-command.ts +314 -0
  25. package/src/commands/db/index.ts +1 -0
  26. package/src/commands/deploy/deploy-command.test.ts +304 -0
  27. package/src/commands/deploy/deploy-command.ts +1053 -0
  28. package/src/commands/deploy/index.ts +2 -0
  29. package/src/commands/deploy/telegram-service.ts +61 -0
  30. package/src/commands/deploy/types.ts +34 -0
  31. package/src/commands/dev/dev-command.test.ts +383 -0
  32. package/src/commands/dev/dev-command.ts +407 -0
  33. package/src/commands/dev/index.ts +1 -0
  34. package/src/commands/infra/index.ts +5 -0
  35. package/src/commands/infra/infra-command.test.ts +719 -0
  36. package/src/commands/infra/infra-command.ts +940 -0
  37. package/src/commands/infra/types.ts +23 -0
  38. package/src/commands/init/index.ts +1 -0
  39. package/src/commands/init/init-command.test.ts +827 -0
  40. package/src/commands/init/init-command.ts +627 -0
  41. package/src/commands/init/types.ts +185 -0
  42. package/src/commands/monitor/index.ts +2 -0
  43. package/src/commands/monitor/monitor-command.test.ts +235 -0
  44. package/src/commands/monitor/monitor-command.ts +245 -0
  45. package/src/commands/monitor/monitor-service.ts +50 -0
  46. package/src/commands/monitor/types.ts +13 -0
  47. package/src/commands/repair/index.ts +2 -0
  48. package/src/commands/repair/repair-command.test.ts +204 -0
  49. package/src/commands/repair/repair-command.ts +199 -0
  50. package/src/commands/repair/repair-service.ts +102 -0
  51. package/src/commands/repair/types.ts +13 -0
  52. package/src/commands/test/index.ts +2 -0
  53. package/src/commands/test/test-command.test.ts +319 -0
  54. package/src/commands/test/test-command.ts +412 -0
  55. package/src/commands/waf/index.ts +2 -0
  56. package/src/commands/waf/types.ts +48 -0
  57. package/src/commands/waf/waf-command.test.ts +506 -0
  58. package/src/commands/waf/waf-command.ts +548 -0
  59. package/src/index.ts +198 -0
  60. package/src/services/cloudflare/cloudflare-service.test.ts +654 -0
  61. package/src/services/cloudflare/cloudflare-service.ts +435 -0
  62. package/src/services/cloudflare/index.ts +2 -0
  63. package/src/services/cloudflare/types.ts +29 -0
  64. package/src/services/config/config-service.test.ts +395 -0
  65. package/src/services/config/config-service.ts +207 -0
  66. package/src/services/config/index.ts +9 -0
  67. package/src/services/config/types.ts +66 -0
  68. package/src/services/db/db-service.test.ts +51 -0
  69. package/src/services/db/db-service.ts +140 -0
  70. package/src/services/db/index.ts +1 -0
  71. package/src/services/docker/docker-service.ts +155 -0
  72. package/src/services/docker/index.ts +1 -0
  73. package/src/services/env/env-service.test.ts +210 -0
  74. package/src/services/env/env-service.ts +156 -0
  75. package/src/services/env/index.ts +1 -0
  76. package/src/services/kv/index.ts +1 -0
  77. package/src/services/kv/kv-sync-service.test.ts +38 -0
  78. package/src/services/kv/kv-sync-service.ts +151 -0
  79. package/src/services/prerequisites/index.ts +1 -0
  80. package/src/services/prerequisites/prerequisites-service.test.ts +89 -0
  81. package/src/services/prerequisites/prerequisites-service.ts +269 -0
  82. package/src/services/prerequisites/types.ts +48 -0
  83. package/src/services/secrets/index.ts +12 -0
  84. package/src/services/secrets/secrets-service.test.ts +486 -0
  85. package/src/services/secrets/secrets-service.ts +293 -0
  86. package/src/services/secrets/types.ts +57 -0
  87. package/src/ui/banner.ts +52 -0
  88. package/src/ui/index.ts +8 -0
  89. package/src/ui/menu.ts +473 -0
  90. package/src/utils/errors.test.ts +69 -0
  91. package/src/utils/errors.ts +23 -0
  92. package/src/utils/formatters.test.ts +180 -0
  93. package/src/utils/formatters.ts +252 -0
  94. package/src/utils/theme.ts +94 -0
@@ -0,0 +1,719 @@
1
+ /**
2
+ * Unit tests for the `hoox infra` command group.
3
+ *
4
+ * Tests each subcommand handler directly by injecting mock CloudflareService
5
+ * and ConfigService instances. Output is captured via process.stdout.write mock.
6
+ */
7
+
8
+ import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
9
+ import {
10
+ doD1List,
11
+ doD1Create,
12
+ doD1Delete,
13
+ doKvList,
14
+ doKvCreate,
15
+ doKvDelete,
16
+ doR2List,
17
+ doR2Create,
18
+ doR2Delete,
19
+ doQueueList,
20
+ doQueueCreate,
21
+ doQueueDelete,
22
+ doProvision,
23
+ displayListResult,
24
+ handleCreate,
25
+ handleDelete,
26
+ registerInfraCommand,
27
+ } from "./infra-command.js";
28
+ import type { InfraOptions } from "./types.js";
29
+ import type { WranglerResult } from "../../services/cloudflare/types.js";
30
+ import { Command } from "commander";
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Output capture helper
34
+ // ---------------------------------------------------------------------------
35
+
36
+ function captureStdout(): { output: () => string; restore: () => void } {
37
+ const chunks: string[] = [];
38
+ const originalWrite = process.stdout.write.bind(process.stdout);
39
+ const writeMock = mock((chunk: string | Buffer) => {
40
+ chunks.push(typeof chunk === "string" ? chunk : chunk.toString());
41
+ return true;
42
+ });
43
+ process.stdout.write = writeMock as unknown as typeof process.stdout.write;
44
+
45
+ return {
46
+ output: () => chunks.join(""),
47
+ restore: () => {
48
+ process.stdout.write = originalWrite;
49
+ },
50
+ };
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Mock CloudflareService builder
55
+ // ---------------------------------------------------------------------------
56
+
57
+ /** Successful wrangler result with data. */
58
+ function okResult<T>(data: T): WranglerResult<T> {
59
+ return { ok: true, data } as WranglerResult<T>;
60
+ }
61
+
62
+ /** Failed wrangler result with error message. */
63
+ function errResult(error: string): WranglerResult<string> {
64
+ return { ok: false, error };
65
+ }
66
+
67
+ /** Default JSON list output for D1. */
68
+ const D1_LIST_JSON = JSON.stringify([
69
+ { name: "test-db", uuid: "abc-123", version: "production", num_tables: 3 },
70
+ { name: "other-db", uuid: "def-456", version: "production", num_tables: 1 },
71
+ ]);
72
+
73
+ /** Default JSON list output for KV. */
74
+ const KV_LIST_JSON = JSON.stringify([
75
+ { id: "kv-abc", title: "config-kv", supports_url_encoding: true },
76
+ ]);
77
+
78
+ /** Default JSON list output for R2. */
79
+ const R2_LIST_JSON = JSON.stringify([
80
+ { name: "my-bucket", creation_date: "2024-01-15T00:00:00Z" },
81
+ ]);
82
+
83
+ /** Default JSON list output for Queues. */
84
+ const QUEUE_LIST_JSON = JSON.stringify([
85
+ { queue_name: "trade-queue", created_on: "2024-06-01T00:00:00Z" },
86
+ ]);
87
+
88
+ interface MockCloudflareService {
89
+ d1List: () => Promise<WranglerResult<string>>;
90
+ d1Create: (name: string) => Promise<WranglerResult<string>>;
91
+ d1Delete: (name: string) => Promise<WranglerResult<string>>;
92
+ kvList: () => Promise<WranglerResult<string>>;
93
+ kvCreate: (name: string) => Promise<WranglerResult<string>>;
94
+ kvDelete: (id: string) => Promise<WranglerResult<string>>;
95
+ r2List: () => Promise<WranglerResult<string>>;
96
+ r2Create: (name: string) => Promise<WranglerResult<string>>;
97
+ r2Delete: (name: string) => Promise<WranglerResult<string>>;
98
+ queueList: () => Promise<WranglerResult<string>>;
99
+ queueCreate: (name: string) => Promise<WranglerResult<string>>;
100
+ queueDelete: (name: string) => Promise<WranglerResult<string>>;
101
+ }
102
+
103
+ function createMockCloudflare(
104
+ overrides?: Partial<MockCloudflareService>
105
+ ): MockCloudflareService {
106
+ return {
107
+ d1List: async () => okResult(D1_LIST_JSON),
108
+ d1Create: async (name: string) => okResult(`Created ${name}`),
109
+ d1Delete: async (name: string) => okResult(`Deleted ${name}`),
110
+ kvList: async () => okResult(KV_LIST_JSON),
111
+ kvCreate: async (name: string) => okResult(`Created ${name}`),
112
+ kvDelete: async (id: string) => okResult(`Deleted ${id}`),
113
+ r2List: async () => okResult(R2_LIST_JSON),
114
+ r2Create: async (name: string) => okResult(`Created ${name}`),
115
+ r2Delete: async (name: string) => okResult(`Deleted ${name}`),
116
+ queueList: async () => okResult(QUEUE_LIST_JSON),
117
+ queueCreate: async (name: string) => okResult(`Created ${name}`),
118
+ queueDelete: async (name: string) => okResult(`Deleted ${name}`),
119
+ ...overrides,
120
+ };
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Tests
125
+ // ---------------------------------------------------------------------------
126
+
127
+ describe("infra-command", () => {
128
+ let capture: ReturnType<typeof captureStdout>;
129
+ const humanOpts: InfraOptions = { json: false, quiet: false };
130
+ const jsonOpts: InfraOptions = { json: true, quiet: false };
131
+ const quietOpts: InfraOptions = { json: false, quiet: true };
132
+
133
+ beforeEach(() => {
134
+ capture = captureStdout();
135
+ });
136
+
137
+ afterEach(() => {
138
+ capture.restore();
139
+ mock.restore();
140
+ });
141
+
142
+ // -- registerInfraCommand -------------------------------------------------
143
+
144
+ describe("registerInfraCommand", () => {
145
+ it("registers infra command with subcommands on a program", () => {
146
+ const program = new Command();
147
+ program.exitOverride();
148
+
149
+ registerInfraCommand(program);
150
+
151
+ // Verify the infra command exists
152
+ const infraCmd = program.commands.find((c) => c.name() === "infra");
153
+ expect(infraCmd).toBeDefined();
154
+
155
+ // Verify subcommands exist
156
+ const subNames = infraCmd!.commands.map((c) => c.name());
157
+ expect(subNames).toContain("provision");
158
+ expect(subNames).toContain("d1");
159
+ expect(subNames).toContain("kv");
160
+ expect(subNames).toContain("r2");
161
+ expect(subNames).toContain("queues");
162
+
163
+ // Verify d1 subcommands
164
+ const d1Cmd = infraCmd!.commands.find((c) => c.name() === "d1");
165
+ expect(d1Cmd).toBeDefined();
166
+ const d1Subs = d1Cmd!.commands.map((c) => c.name());
167
+ expect(d1Subs).toContain("list");
168
+ expect(d1Subs).toContain("create");
169
+ expect(d1Subs).toContain("delete");
170
+ });
171
+ });
172
+
173
+ // -- displayListResult ----------------------------------------------------
174
+
175
+ describe("displayListResult", () => {
176
+ it("displays JSON array as a table in human mode", () => {
177
+ const result = okResult(D1_LIST_JSON);
178
+ displayListResult(result, humanOpts, ["name", "uuid"]);
179
+
180
+ const out = capture.output();
181
+ expect(out).toContain("test-db");
182
+ expect(out).toContain("abc-123");
183
+ expect(out).toContain("other-db");
184
+ expect(out).toContain("def-456");
185
+ });
186
+
187
+ it("outputs raw JSON array in json mode", () => {
188
+ const result = okResult(D1_LIST_JSON);
189
+ displayListResult(result, jsonOpts, ["name", "uuid"]);
190
+
191
+ const out = capture.output();
192
+ const parsed = JSON.parse(out);
193
+ expect(Array.isArray(parsed)).toBe(true);
194
+ expect(parsed[0].name).toBe("test-db");
195
+ });
196
+
197
+ it("outputs nothing in quiet mode", () => {
198
+ const result = okResult(D1_LIST_JSON);
199
+ displayListResult(result, quietOpts, ["name"]);
200
+
201
+ const out = capture.output();
202
+ expect(out).toBe("");
203
+ });
204
+
205
+ it("falls back to raw text when output is not JSON", () => {
206
+ const result = okResult("Raw text output from wrangler");
207
+ displayListResult(result, humanOpts);
208
+
209
+ const out = capture.output();
210
+ expect(out).toContain("Raw text output from wrangler");
211
+ });
212
+
213
+ it("displays error when result is not ok", () => {
214
+ const result = errResult("Authentication failed");
215
+ displayListResult(result, humanOpts);
216
+
217
+ const out = capture.output();
218
+ expect(out).toContain("Authentication failed");
219
+ });
220
+
221
+ it("displays (empty) for empty array", () => {
222
+ const result = okResult("[]");
223
+ displayListResult(result, humanOpts);
224
+ // Falls through to raw text since try/catch catches empty array
225
+ // (our helper only attempts table for non-empty arrays)
226
+ const out = capture.output();
227
+ expect(out).toContain("[]");
228
+ });
229
+ });
230
+
231
+ // -- handleCreate ---------------------------------------------------------
232
+
233
+ describe("handleCreate", () => {
234
+ it("shows success message on creation", async () => {
235
+ await handleCreate(
236
+ "my-resource",
237
+ "D1 database",
238
+ async () => okResult("created"),
239
+ humanOpts
240
+ );
241
+
242
+ const out = capture.output();
243
+ expect(out).toContain("Creating D1 database:");
244
+ expect(out).toContain("my-resource");
245
+ });
246
+
247
+ it("shows error message on failure", async () => {
248
+ await handleCreate(
249
+ "bad-resource",
250
+ "D1 database",
251
+ async () => errResult("Already exists"),
252
+ humanOpts
253
+ );
254
+
255
+ const out = capture.output();
256
+ expect(out).toContain("Already exists");
257
+ });
258
+ });
259
+
260
+ // -- handleDelete ---------------------------------------------------------
261
+
262
+ describe("handleDelete", () => {
263
+ it("shows success message on deletion", async () => {
264
+ await handleDelete(
265
+ "my-resource",
266
+ "D1 database",
267
+ async () => okResult("deleted"),
268
+ humanOpts
269
+ );
270
+
271
+ const out = capture.output();
272
+ expect(out).toContain("Deleting D1 database:");
273
+ expect(out).toContain("my-resource");
274
+ });
275
+
276
+ it("shows error message on failure", async () => {
277
+ await handleDelete(
278
+ "missing",
279
+ "D1 database",
280
+ async () => errResult("Not found"),
281
+ humanOpts
282
+ );
283
+
284
+ const out = capture.output();
285
+ expect(out).toContain("Not found");
286
+ });
287
+ });
288
+
289
+ // -- D1 handlers ----------------------------------------------------------
290
+
291
+ describe("doD1List", () => {
292
+ it("shows D1 databases as a table", async () => {
293
+ const mockCf = createMockCloudflare();
294
+ await doD1List(
295
+ humanOpts,
296
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
297
+ );
298
+
299
+ const out = capture.output();
300
+ expect(out).toContain("test-db");
301
+ expect(out).toContain("other-db");
302
+ });
303
+ });
304
+
305
+ describe("doD1Create", () => {
306
+ it("creates a D1 database and shows success", async () => {
307
+ const mockCf = createMockCloudflare();
308
+ await doD1Create(
309
+ "new-db",
310
+ humanOpts,
311
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
312
+ );
313
+
314
+ const out = capture.output();
315
+ expect(out).toContain("new-db");
316
+ });
317
+ });
318
+
319
+ describe("doD1Delete", () => {
320
+ it("deletes a D1 database and shows success", async () => {
321
+ const mockCf = createMockCloudflare();
322
+ await doD1Delete(
323
+ "old-db",
324
+ humanOpts,
325
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
326
+ );
327
+
328
+ const out = capture.output();
329
+ expect(out).toContain("old-db");
330
+ });
331
+ });
332
+
333
+ // -- KV handlers ----------------------------------------------------------
334
+
335
+ describe("doKvList", () => {
336
+ it("shows KV namespaces", async () => {
337
+ const mockCf = createMockCloudflare();
338
+ await doKvList(
339
+ humanOpts,
340
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
341
+ );
342
+
343
+ const out = capture.output();
344
+ expect(out).toContain("config-kv");
345
+ });
346
+ });
347
+
348
+ describe("doKvCreate", () => {
349
+ it("creates a KV namespace", async () => {
350
+ const mockCf = createMockCloudflare();
351
+ await doKvCreate(
352
+ "my-kv",
353
+ humanOpts,
354
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
355
+ );
356
+
357
+ const out = capture.output();
358
+ expect(out).toContain("my-kv");
359
+ });
360
+ });
361
+
362
+ describe("doKvDelete", () => {
363
+ it("deletes a KV namespace by ID", async () => {
364
+ const mockCf = createMockCloudflare();
365
+ await doKvDelete(
366
+ "kv-abc",
367
+ humanOpts,
368
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
369
+ );
370
+
371
+ const out = capture.output();
372
+ expect(out).toContain("kv-abc");
373
+ });
374
+ });
375
+
376
+ // -- R2 handlers ----------------------------------------------------------
377
+
378
+ describe("doR2List", () => {
379
+ it("shows R2 buckets", async () => {
380
+ const mockCf = createMockCloudflare();
381
+ await doR2List(
382
+ humanOpts,
383
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
384
+ );
385
+
386
+ const out = capture.output();
387
+ expect(out).toContain("my-bucket");
388
+ });
389
+ });
390
+
391
+ describe("doR2Create", () => {
392
+ it("creates an R2 bucket", async () => {
393
+ const mockCf = createMockCloudflare();
394
+ await doR2Create(
395
+ "new-bucket",
396
+ humanOpts,
397
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
398
+ );
399
+
400
+ const out = capture.output();
401
+ expect(out).toContain("new-bucket");
402
+ });
403
+ });
404
+
405
+ describe("doR2Delete", () => {
406
+ it("deletes an R2 bucket", async () => {
407
+ const mockCf = createMockCloudflare();
408
+ await doR2Delete(
409
+ "old-bucket",
410
+ humanOpts,
411
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
412
+ );
413
+
414
+ const out = capture.output();
415
+ expect(out).toContain("old-bucket");
416
+ });
417
+ });
418
+
419
+ // -- Queue handlers -------------------------------------------------------
420
+
421
+ describe("doQueueList", () => {
422
+ it("shows Queues", async () => {
423
+ const mockCf = createMockCloudflare();
424
+ await doQueueList(
425
+ humanOpts,
426
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
427
+ );
428
+
429
+ const out = capture.output();
430
+ expect(out).toContain("trade-queue");
431
+ });
432
+ });
433
+
434
+ describe("doQueueCreate", () => {
435
+ it("creates a Queue", async () => {
436
+ const mockCf = createMockCloudflare();
437
+ await doQueueCreate(
438
+ "new-queue",
439
+ humanOpts,
440
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
441
+ );
442
+
443
+ const out = capture.output();
444
+ expect(out).toContain("new-queue");
445
+ });
446
+ });
447
+
448
+ describe("doQueueDelete", () => {
449
+ it("deletes a Queue", async () => {
450
+ const mockCf = createMockCloudflare();
451
+ await doQueueDelete(
452
+ "old-queue",
453
+ humanOpts,
454
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
455
+ );
456
+
457
+ const out = capture.output();
458
+ expect(out).toContain("old-queue");
459
+ });
460
+ });
461
+
462
+ // -- JSON output mode -----------------------------------------------------
463
+
464
+ describe("JSON output mode", () => {
465
+ it("outputs D1 list as JSON", async () => {
466
+ const mockCf = createMockCloudflare();
467
+ await doD1List(
468
+ jsonOpts,
469
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
470
+ );
471
+
472
+ const out = capture.output();
473
+ const parsed = JSON.parse(out);
474
+ expect(Array.isArray(parsed)).toBe(true);
475
+ expect(parsed[0].name).toBe("test-db");
476
+ });
477
+
478
+ it("outputs KV list as JSON", async () => {
479
+ const mockCf = createMockCloudflare();
480
+ await doKvList(
481
+ jsonOpts,
482
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
483
+ );
484
+
485
+ const out = capture.output();
486
+ const parsed = JSON.parse(out);
487
+ expect(Array.isArray(parsed)).toBe(true);
488
+ expect(parsed[0].id).toBe("kv-abc");
489
+ });
490
+ });
491
+
492
+ // -- Quiet mode -----------------------------------------------------------
493
+
494
+ describe("Quiet mode", () => {
495
+ it("produces no output for list in quiet mode", async () => {
496
+ const mockCf = createMockCloudflare();
497
+ await doD1List(
498
+ quietOpts,
499
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
500
+ );
501
+
502
+ expect(capture.output()).toBe("");
503
+ });
504
+ });
505
+
506
+ // -- Error handling -------------------------------------------------------
507
+
508
+ describe("error propagation", () => {
509
+ it("shows error for failed D1 list", async () => {
510
+ const mockCf = createMockCloudflare({
511
+ d1List: async () => errResult("wrangler not authenticated"),
512
+ });
513
+ await doD1List(
514
+ humanOpts,
515
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
516
+ );
517
+
518
+ const out = capture.output();
519
+ expect(out).toContain("wrangler not authenticated");
520
+ });
521
+
522
+ it("shows error for failed D1 create", async () => {
523
+ const mockCf = createMockCloudflare({
524
+ d1Create: async () => errResult("database already exists"),
525
+ });
526
+ await doD1Create(
527
+ "dup-db",
528
+ humanOpts,
529
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
530
+ );
531
+
532
+ const out = capture.output();
533
+ expect(out).toContain("database already exists");
534
+ });
535
+
536
+ it("shows error for failed queue delete", async () => {
537
+ const mockCf = createMockCloudflare({
538
+ queueDelete: async () => errResult("queue not found"),
539
+ });
540
+ await doQueueDelete(
541
+ "ghost",
542
+ humanOpts,
543
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService
544
+ );
545
+
546
+ const out = capture.output();
547
+ expect(out).toContain("queue not found");
548
+ });
549
+ });
550
+
551
+ // -- Provision ------------------------------------------------------------
552
+
553
+ describe("doProvision", () => {
554
+ // Save references to real implementations for restoration
555
+ const realFile = Bun.file;
556
+
557
+ afterEach(() => {
558
+ (Bun as unknown as Record<string, unknown>).file = realFile;
559
+ });
560
+
561
+ it("returns empty result when no enabled workers", async () => {
562
+ const mockConfig = {
563
+ load: async () => ({ global: {}, workers: {} }),
564
+ listEnabledWorkers: () => [] as string[],
565
+ getWorker: () => undefined,
566
+ };
567
+
568
+ const mockCf = createMockCloudflare();
569
+ const result = await doProvision(
570
+ humanOpts,
571
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService,
572
+ mockConfig as unknown as import("../../services/config/config-service.js").ConfigService
573
+ );
574
+
575
+ expect(result.items).toEqual([]);
576
+ expect(result.summary.total).toBe(0);
577
+ });
578
+
579
+ it("handles config load failure gracefully", async () => {
580
+ const mockConfig = {
581
+ load: async () => {
582
+ throw new Error("Config file not found");
583
+ },
584
+ listEnabledWorkers: () => [] as string[],
585
+ getWorker: () => undefined,
586
+ };
587
+
588
+ const mockCf = createMockCloudflare();
589
+ const result = await doProvision(
590
+ humanOpts,
591
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService,
592
+ mockConfig as unknown as import("../../services/config/config-service.js").ConfigService
593
+ );
594
+
595
+ expect(result.items).toEqual([]);
596
+ expect(result.summary.total).toBe(0);
597
+
598
+ const out = capture.output();
599
+ expect(out).toContain("Config file not found");
600
+ });
601
+
602
+ it("provisions D1 databases from worker wrangler.jsonc", async () => {
603
+ const workerPath = "/fake/workers/test-worker";
604
+
605
+ const mockConfig = {
606
+ load: async () => ({
607
+ global: {},
608
+ workers: { "test-worker": { enabled: true, path: workerPath } },
609
+ }),
610
+ listEnabledWorkers: () => ["test-worker"],
611
+ getWorker: (name: string) =>
612
+ name === "test-worker"
613
+ ? { enabled: true, path: workerPath }
614
+ : undefined,
615
+ };
616
+
617
+ const wranglerConfig = JSON.stringify({
618
+ d1_databases: [
619
+ { binding: "DB", database_name: "my-db", database_id: "xxx" },
620
+ ],
621
+ });
622
+
623
+ // Mock Bun.file to return our fake wrangler.jsonc
624
+ (Bun as unknown as Record<string, unknown>).file = mock((p: string) => ({
625
+ exists: async () => p === `${workerPath}/wrangler.jsonc`,
626
+ text: async () => wranglerConfig,
627
+ }));
628
+
629
+ const mockCf = createMockCloudflare({
630
+ d1Create: async () => okResult("Created my-db"),
631
+ });
632
+
633
+ const result = await doProvision(
634
+ humanOpts,
635
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService,
636
+ mockConfig as unknown as import("../../services/config/config-service.js").ConfigService
637
+ );
638
+
639
+ expect(result.items.length).toBeGreaterThan(0);
640
+ const d1Item = result.items.find(
641
+ (i) => i.name === "my-db" && i.type === "d1"
642
+ );
643
+ expect(d1Item).toBeDefined();
644
+ expect(d1Item!.status).toBe("created");
645
+ });
646
+
647
+ it("marks resources as exists when wrangler reports already exists", async () => {
648
+ const workerPath = "/fake/workers/existing-worker";
649
+
650
+ const mockConfig = {
651
+ load: async () => ({
652
+ global: {},
653
+ workers: { "existing-worker": { enabled: true, path: workerPath } },
654
+ }),
655
+ listEnabledWorkers: () => ["existing-worker"],
656
+ getWorker: (name: string) =>
657
+ name === "existing-worker"
658
+ ? { enabled: true, path: workerPath }
659
+ : undefined,
660
+ };
661
+
662
+ const wranglerConfig = JSON.stringify({
663
+ r2_buckets: [{ binding: "FILES", bucket_name: "storage" }],
664
+ });
665
+
666
+ (Bun as unknown as Record<string, unknown>).file = mock((p: string) => ({
667
+ exists: async () => p === `${workerPath}/wrangler.jsonc`,
668
+ text: async () => wranglerConfig,
669
+ }));
670
+
671
+ const mockCf = createMockCloudflare({
672
+ r2Create: async () => errResult("bucket already exists"),
673
+ });
674
+
675
+ const result = await doProvision(
676
+ humanOpts,
677
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService,
678
+ mockConfig as unknown as import("../../services/config/config-service.js").ConfigService
679
+ );
680
+
681
+ const r2Item = result.items.find(
682
+ (i) => i.name === "storage" && i.type === "r2"
683
+ );
684
+ expect(r2Item).toBeDefined();
685
+ expect(r2Item!.status).toBe("exists");
686
+ });
687
+
688
+ it("handles workers without wrangler.jsonc gracefully", async () => {
689
+ const workerPath = "/fake/workers/no-config-worker";
690
+
691
+ const mockConfig = {
692
+ load: async () => ({
693
+ global: {},
694
+ workers: { "no-config-worker": { enabled: true, path: workerPath } },
695
+ }),
696
+ listEnabledWorkers: () => ["no-config-worker"],
697
+ getWorker: (name: string) =>
698
+ name === "no-config-worker"
699
+ ? { enabled: true, path: workerPath }
700
+ : undefined,
701
+ };
702
+
703
+ (Bun as unknown as Record<string, unknown>).file = mock(() => ({
704
+ exists: async () => false,
705
+ text: async () => "",
706
+ }));
707
+
708
+ const mockCf = createMockCloudflare();
709
+ const result = await doProvision(
710
+ humanOpts,
711
+ mockCf as unknown as import("../../services/cloudflare/cloudflare-service.js").CloudflareService,
712
+ mockConfig as unknown as import("../../services/config/config-service.js").ConfigService
713
+ );
714
+
715
+ // No wrangler.jsonc → no items to provision
716
+ expect(result.items).toEqual([]);
717
+ });
718
+ });
719
+ });