@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.
- package/README.md +403 -0
- package/bin/hoox.js +12 -0
- package/package.json +60 -0
- package/src/commands/check/check-command.test.ts +468 -0
- package/src/commands/check/check-command.ts +1144 -0
- package/src/commands/check/index.ts +10 -0
- package/src/commands/check/prerequisites-command.test.ts +19 -0
- package/src/commands/check/prerequisites-command.ts +92 -0
- package/src/commands/check/types.ts +103 -0
- package/src/commands/clone/clone-command.test.ts +442 -0
- package/src/commands/clone/clone-command.ts +440 -0
- package/src/commands/clone/index.ts +1 -0
- package/src/commands/config/config-command.test.ts +583 -0
- package/src/commands/config/config-command.ts +901 -0
- package/src/commands/config/env-command.test.ts +43 -0
- package/src/commands/config/env-command.ts +314 -0
- package/src/commands/config/index.ts +3 -0
- package/src/commands/config/kv-command.test.ts +14 -0
- package/src/commands/config/kv-command.ts +329 -0
- package/src/commands/dashboard/dashboard-command.test.ts +47 -0
- package/src/commands/dashboard/dashboard-command.ts +127 -0
- package/src/commands/dashboard/index.ts +1 -0
- package/src/commands/db/db-command.test.ts +21 -0
- package/src/commands/db/db-command.ts +314 -0
- package/src/commands/db/index.ts +1 -0
- package/src/commands/deploy/deploy-command.test.ts +304 -0
- package/src/commands/deploy/deploy-command.ts +1053 -0
- package/src/commands/deploy/index.ts +2 -0
- package/src/commands/deploy/telegram-service.ts +61 -0
- package/src/commands/deploy/types.ts +34 -0
- package/src/commands/dev/dev-command.test.ts +383 -0
- package/src/commands/dev/dev-command.ts +407 -0
- package/src/commands/dev/index.ts +1 -0
- package/src/commands/infra/index.ts +5 -0
- package/src/commands/infra/infra-command.test.ts +719 -0
- package/src/commands/infra/infra-command.ts +940 -0
- package/src/commands/infra/types.ts +23 -0
- package/src/commands/init/index.ts +1 -0
- package/src/commands/init/init-command.test.ts +827 -0
- package/src/commands/init/init-command.ts +627 -0
- package/src/commands/init/types.ts +185 -0
- package/src/commands/monitor/index.ts +2 -0
- package/src/commands/monitor/monitor-command.test.ts +235 -0
- package/src/commands/monitor/monitor-command.ts +245 -0
- package/src/commands/monitor/monitor-service.ts +50 -0
- package/src/commands/monitor/types.ts +13 -0
- package/src/commands/repair/index.ts +2 -0
- package/src/commands/repair/repair-command.test.ts +204 -0
- package/src/commands/repair/repair-command.ts +199 -0
- package/src/commands/repair/repair-service.ts +102 -0
- package/src/commands/repair/types.ts +13 -0
- package/src/commands/test/index.ts +2 -0
- package/src/commands/test/test-command.test.ts +319 -0
- package/src/commands/test/test-command.ts +412 -0
- package/src/commands/waf/index.ts +2 -0
- package/src/commands/waf/types.ts +48 -0
- package/src/commands/waf/waf-command.test.ts +506 -0
- package/src/commands/waf/waf-command.ts +548 -0
- package/src/index.ts +198 -0
- package/src/services/cloudflare/cloudflare-service.test.ts +654 -0
- package/src/services/cloudflare/cloudflare-service.ts +435 -0
- package/src/services/cloudflare/index.ts +2 -0
- package/src/services/cloudflare/types.ts +29 -0
- package/src/services/config/config-service.test.ts +395 -0
- package/src/services/config/config-service.ts +207 -0
- package/src/services/config/index.ts +9 -0
- package/src/services/config/types.ts +66 -0
- package/src/services/db/db-service.test.ts +51 -0
- package/src/services/db/db-service.ts +140 -0
- package/src/services/db/index.ts +1 -0
- package/src/services/docker/docker-service.ts +155 -0
- package/src/services/docker/index.ts +1 -0
- package/src/services/env/env-service.test.ts +210 -0
- package/src/services/env/env-service.ts +156 -0
- package/src/services/env/index.ts +1 -0
- package/src/services/kv/index.ts +1 -0
- package/src/services/kv/kv-sync-service.test.ts +38 -0
- package/src/services/kv/kv-sync-service.ts +151 -0
- package/src/services/prerequisites/index.ts +1 -0
- package/src/services/prerequisites/prerequisites-service.test.ts +89 -0
- package/src/services/prerequisites/prerequisites-service.ts +269 -0
- package/src/services/prerequisites/types.ts +48 -0
- package/src/services/secrets/index.ts +12 -0
- package/src/services/secrets/secrets-service.test.ts +486 -0
- package/src/services/secrets/secrets-service.ts +293 -0
- package/src/services/secrets/types.ts +57 -0
- package/src/ui/banner.ts +52 -0
- package/src/ui/index.ts +8 -0
- package/src/ui/menu.ts +473 -0
- package/src/utils/errors.test.ts +69 -0
- package/src/utils/errors.ts +23 -0
- package/src/utils/formatters.test.ts +180 -0
- package/src/utils/formatters.ts +252 -0
- package/src/utils/theme.ts +94 -0
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
|
|
2
|
+
import { CloudflareService } from "./cloudflare-service.js";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/** Save a reference to the real Bun.spawn so we can restore it after tests. */
|
|
9
|
+
const realSpawn = Bun.spawn;
|
|
10
|
+
|
|
11
|
+
type MockSpawnResult = {
|
|
12
|
+
stdout: Blob;
|
|
13
|
+
stderr: Blob;
|
|
14
|
+
exited: Promise<number>;
|
|
15
|
+
stdin?: {
|
|
16
|
+
write: ReturnType<typeof mock>;
|
|
17
|
+
end: ReturnType<typeof mock>;
|
|
18
|
+
};
|
|
19
|
+
kill: ReturnType<typeof mock>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a mock spawn result with the given stdout, stderr, and exit code.
|
|
24
|
+
* Used to replace Bun.spawn in tests.
|
|
25
|
+
*/
|
|
26
|
+
function makeSpawnResult(
|
|
27
|
+
stdoutText: string,
|
|
28
|
+
stderrText: string,
|
|
29
|
+
exitCode: number
|
|
30
|
+
): MockSpawnResult {
|
|
31
|
+
return {
|
|
32
|
+
stdout: new Blob([stdoutText]),
|
|
33
|
+
stderr: new Blob([stderrText]),
|
|
34
|
+
exited: Promise.resolve(exitCode),
|
|
35
|
+
stdin: {
|
|
36
|
+
write: mock(() => {}),
|
|
37
|
+
end: mock(() => {}),
|
|
38
|
+
},
|
|
39
|
+
kill: mock(() => {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Convenience: a successful spawn with the given stdout.
|
|
45
|
+
*/
|
|
46
|
+
function successSpawn(stdout: string): MockSpawnResult {
|
|
47
|
+
return makeSpawnResult(stdout, "", 0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Convenience: a failed spawn with the given stderr and exit code.
|
|
52
|
+
*/
|
|
53
|
+
function errorSpawn(stderr: string, exitCode = 1): MockSpawnResult {
|
|
54
|
+
return makeSpawnResult("", stderr, exitCode);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Install a mock Bun.spawn that returns the given result. */
|
|
58
|
+
function mockSpawnWith(result: MockSpawnResult): void {
|
|
59
|
+
const spawnMock = mock(() => result);
|
|
60
|
+
(Bun as unknown as Record<string, unknown>).spawn = spawnMock;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Track spawn calls so we can assert on arguments. */
|
|
64
|
+
let lastSpawnCmd: string[] = [];
|
|
65
|
+
let lastSpawnCwd: string | undefined;
|
|
66
|
+
|
|
67
|
+
function mockSpawnWithCapture(result: MockSpawnResult): void {
|
|
68
|
+
const spawnMock = mock((cmd: string[], options?: { cwd?: string }) => {
|
|
69
|
+
lastSpawnCmd = cmd;
|
|
70
|
+
lastSpawnCwd = options?.cwd;
|
|
71
|
+
return result;
|
|
72
|
+
});
|
|
73
|
+
(Bun as unknown as Record<string, unknown>).spawn = spawnMock;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Setup / Teardown
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
lastSpawnCmd = [];
|
|
82
|
+
lastSpawnCwd = undefined;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
(Bun as unknown as Record<string, unknown>).spawn = realSpawn;
|
|
87
|
+
mock.restore();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Tests
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
describe("CloudflareService", () => {
|
|
95
|
+
// -- Constructor ----------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
it("defaults cwd to process.cwd()", () => {
|
|
98
|
+
const service = new CloudflareService();
|
|
99
|
+
// Cannot easily inspect private cwd, but we verify via spawn cwd later
|
|
100
|
+
expect(service).toBeDefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("accepts a custom cwd", () => {
|
|
104
|
+
const service = new CloudflareService("/custom/path");
|
|
105
|
+
expect(service).toBeDefined();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// -- whoami ---------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
describe("whoami", () => {
|
|
111
|
+
it("returns ok with stdout on success", async () => {
|
|
112
|
+
mockSpawnWithCapture(successSpawn("user@example.com"));
|
|
113
|
+
|
|
114
|
+
const service = new CloudflareService();
|
|
115
|
+
const result = await service.whoami();
|
|
116
|
+
|
|
117
|
+
expect(result.ok).toBe(true);
|
|
118
|
+
if (result.ok) {
|
|
119
|
+
expect(result.data).toBe("user@example.com");
|
|
120
|
+
}
|
|
121
|
+
expect(lastSpawnCmd).toEqual(["wrangler", "whoami"]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("returns error on non-zero exit", async () => {
|
|
125
|
+
mockSpawnWithCapture(errorSpawn("Not authenticated"));
|
|
126
|
+
|
|
127
|
+
const service = new CloudflareService();
|
|
128
|
+
const result = await service.whoami();
|
|
129
|
+
|
|
130
|
+
expect(result.ok).toBe(false);
|
|
131
|
+
if (!result.ok) {
|
|
132
|
+
expect(result.error).toContain("Not authenticated");
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// -- deploy ---------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
describe("deploy", () => {
|
|
140
|
+
it("deploys without --env by default", async () => {
|
|
141
|
+
const stdout =
|
|
142
|
+
"Published test-worker (0.5 sec)\n https://test-worker.cryptolinx.workers.dev";
|
|
143
|
+
mockSpawnWithCapture(successSpawn(stdout));
|
|
144
|
+
|
|
145
|
+
const service = new CloudflareService();
|
|
146
|
+
const result = await service.deploy("workers/test-worker");
|
|
147
|
+
|
|
148
|
+
expect(result.ok).toBe(true);
|
|
149
|
+
if (result.ok) {
|
|
150
|
+
expect(result.data.url).toBe(
|
|
151
|
+
"https://test-worker.cryptolinx.workers.dev"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
expect(lastSpawnCmd).toEqual(["wrangler", "deploy"]);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("includes --env when specified", async () => {
|
|
158
|
+
const stdout =
|
|
159
|
+
"Published test-worker (production)\n https://test-worker.cryptolinx.workers.dev";
|
|
160
|
+
mockSpawnWithCapture(successSpawn(stdout));
|
|
161
|
+
|
|
162
|
+
const service = new CloudflareService();
|
|
163
|
+
const result = await service.deploy("workers/test-worker", "production");
|
|
164
|
+
|
|
165
|
+
expect(result.ok).toBe(true);
|
|
166
|
+
if (result.ok) {
|
|
167
|
+
expect(result.data.url).toBe(
|
|
168
|
+
"https://test-worker.cryptolinx.workers.dev"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
expect(lastSpawnCmd).toEqual([
|
|
172
|
+
"wrangler",
|
|
173
|
+
"deploy",
|
|
174
|
+
"--env",
|
|
175
|
+
"production",
|
|
176
|
+
]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("returns url undefined when stdout has no URL", async () => {
|
|
180
|
+
mockSpawnWithCapture(successSpawn("Deployed successfully. No URL here."));
|
|
181
|
+
|
|
182
|
+
const service = new CloudflareService();
|
|
183
|
+
const result = await service.deploy("workers/test-worker");
|
|
184
|
+
|
|
185
|
+
expect(result.ok).toBe(true);
|
|
186
|
+
if (result.ok) {
|
|
187
|
+
expect(result.data.url).toBeUndefined();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("returns error on deploy failure", async () => {
|
|
192
|
+
mockSpawnWithCapture(errorSpawn("Authentication error"));
|
|
193
|
+
|
|
194
|
+
const service = new CloudflareService();
|
|
195
|
+
const result = await service.deploy("workers/broken");
|
|
196
|
+
|
|
197
|
+
expect(result.ok).toBe(false);
|
|
198
|
+
if (!result.ok) {
|
|
199
|
+
expect(result.error).toContain("Authentication error");
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// -- dev ------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
describe("dev", () => {
|
|
207
|
+
it("returns default port 8787", async () => {
|
|
208
|
+
mockSpawnWithCapture(successSpawn(""));
|
|
209
|
+
|
|
210
|
+
const service = new CloudflareService();
|
|
211
|
+
const result = await service.dev("workers/test-worker");
|
|
212
|
+
|
|
213
|
+
expect(result.ok).toBe(true);
|
|
214
|
+
if (result.ok) {
|
|
215
|
+
expect(result.data.port).toBe(8787);
|
|
216
|
+
}
|
|
217
|
+
expect(lastSpawnCmd).toEqual(["wrangler", "dev", "--port", "8787"]);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("returns custom port when specified", async () => {
|
|
221
|
+
mockSpawnWithCapture(successSpawn(""));
|
|
222
|
+
|
|
223
|
+
const service = new CloudflareService();
|
|
224
|
+
const result = await service.dev("workers/test-worker", 3000);
|
|
225
|
+
|
|
226
|
+
expect(result.ok).toBe(true);
|
|
227
|
+
if (result.ok) {
|
|
228
|
+
expect(result.data.port).toBe(3000);
|
|
229
|
+
}
|
|
230
|
+
expect(lastSpawnCmd).toEqual(["wrangler", "dev", "--port", "3000"]);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// -- D1 -------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
describe("d1 operations", () => {
|
|
237
|
+
it("d1List calls wrangler d1 list --json", async () => {
|
|
238
|
+
mockSpawnWithCapture(successSpawn('[{"name":"test-db"}]'));
|
|
239
|
+
|
|
240
|
+
const service = new CloudflareService();
|
|
241
|
+
const result = await service.d1List();
|
|
242
|
+
|
|
243
|
+
expect(result.ok).toBe(true);
|
|
244
|
+
expect(lastSpawnCmd).toEqual(["wrangler", "d1", "list", "--json"]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("d1Create calls wrangler d1 create with name", async () => {
|
|
248
|
+
mockSpawnWithCapture(successSpawn("Created database my-db"));
|
|
249
|
+
|
|
250
|
+
const service = new CloudflareService();
|
|
251
|
+
const result = await service.d1Create("my-db");
|
|
252
|
+
|
|
253
|
+
expect(result.ok).toBe(true);
|
|
254
|
+
expect(lastSpawnCmd).toEqual(["wrangler", "d1", "create", "my-db"]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("d1Delete calls wrangler d1 delete with name", async () => {
|
|
258
|
+
mockSpawnWithCapture(successSpawn("Deleted database old-db"));
|
|
259
|
+
|
|
260
|
+
const service = new CloudflareService();
|
|
261
|
+
const result = await service.d1Delete("old-db");
|
|
262
|
+
|
|
263
|
+
expect(result.ok).toBe(true);
|
|
264
|
+
expect(lastSpawnCmd).toEqual(["wrangler", "d1", "delete", "old-db"]);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// -- KV -------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
describe("kv operations", () => {
|
|
271
|
+
it("kvList calls wrangler kv namespace list", async () => {
|
|
272
|
+
mockSpawnWithCapture(successSpawn("[]"));
|
|
273
|
+
|
|
274
|
+
const service = new CloudflareService();
|
|
275
|
+
const result = await service.kvList();
|
|
276
|
+
|
|
277
|
+
expect(result.ok).toBe(true);
|
|
278
|
+
expect(lastSpawnCmd).toEqual(["wrangler", "kv", "namespace", "list"]);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("kvCreate calls wrangler kv namespace create", async () => {
|
|
282
|
+
mockSpawnWithCapture(successSpawn("Created namespace my-kv"));
|
|
283
|
+
|
|
284
|
+
const service = new CloudflareService();
|
|
285
|
+
const result = await service.kvCreate("my-kv");
|
|
286
|
+
|
|
287
|
+
expect(result.ok).toBe(true);
|
|
288
|
+
expect(lastSpawnCmd).toEqual([
|
|
289
|
+
"wrangler",
|
|
290
|
+
"kv",
|
|
291
|
+
"namespace",
|
|
292
|
+
"create",
|
|
293
|
+
"my-kv",
|
|
294
|
+
]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("kvDelete calls wrangler kv namespace delete", async () => {
|
|
298
|
+
mockSpawnWithCapture(successSpawn("Deleted namespace"));
|
|
299
|
+
|
|
300
|
+
const service = new CloudflareService();
|
|
301
|
+
const result = await service.kvDelete("abc123");
|
|
302
|
+
|
|
303
|
+
expect(result.ok).toBe(true);
|
|
304
|
+
expect(lastSpawnCmd).toEqual([
|
|
305
|
+
"wrangler",
|
|
306
|
+
"kv",
|
|
307
|
+
"namespace",
|
|
308
|
+
"delete",
|
|
309
|
+
"--namespace-id",
|
|
310
|
+
"abc123",
|
|
311
|
+
]);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// -- R2 -------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
describe("r2 operations", () => {
|
|
318
|
+
it("r2List calls wrangler r2 bucket list", async () => {
|
|
319
|
+
mockSpawnWithCapture(successSpawn("[]"));
|
|
320
|
+
|
|
321
|
+
const service = new CloudflareService();
|
|
322
|
+
const result = await service.r2List();
|
|
323
|
+
|
|
324
|
+
expect(result.ok).toBe(true);
|
|
325
|
+
expect(lastSpawnCmd).toEqual(["wrangler", "r2", "bucket", "list"]);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("r2Create calls wrangler r2 bucket create", async () => {
|
|
329
|
+
mockSpawnWithCapture(successSpawn("Created bucket my-bucket"));
|
|
330
|
+
|
|
331
|
+
const service = new CloudflareService();
|
|
332
|
+
const result = await service.r2Create("my-bucket");
|
|
333
|
+
|
|
334
|
+
expect(result.ok).toBe(true);
|
|
335
|
+
expect(lastSpawnCmd).toEqual([
|
|
336
|
+
"wrangler",
|
|
337
|
+
"r2",
|
|
338
|
+
"bucket",
|
|
339
|
+
"create",
|
|
340
|
+
"my-bucket",
|
|
341
|
+
]);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("r2Delete calls wrangler r2 bucket delete", async () => {
|
|
345
|
+
mockSpawnWithCapture(successSpawn("Deleted bucket"));
|
|
346
|
+
|
|
347
|
+
const service = new CloudflareService();
|
|
348
|
+
const result = await service.r2Delete("old-bucket");
|
|
349
|
+
|
|
350
|
+
expect(result.ok).toBe(true);
|
|
351
|
+
expect(lastSpawnCmd).toEqual([
|
|
352
|
+
"wrangler",
|
|
353
|
+
"r2",
|
|
354
|
+
"bucket",
|
|
355
|
+
"delete",
|
|
356
|
+
"old-bucket",
|
|
357
|
+
]);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// -- Queues ---------------------------------------------------------------
|
|
362
|
+
|
|
363
|
+
describe("queue operations", () => {
|
|
364
|
+
it("queueList calls wrangler queues list", async () => {
|
|
365
|
+
mockSpawnWithCapture(successSpawn("[]"));
|
|
366
|
+
|
|
367
|
+
const service = new CloudflareService();
|
|
368
|
+
const result = await service.queueList();
|
|
369
|
+
|
|
370
|
+
expect(result.ok).toBe(true);
|
|
371
|
+
expect(lastSpawnCmd).toEqual(["wrangler", "queues", "list"]);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("queueCreate calls wrangler queues create", async () => {
|
|
375
|
+
mockSpawnWithCapture(successSpawn("Created queue my-queue"));
|
|
376
|
+
|
|
377
|
+
const service = new CloudflareService();
|
|
378
|
+
const result = await service.queueCreate("my-queue");
|
|
379
|
+
|
|
380
|
+
expect(result.ok).toBe(true);
|
|
381
|
+
expect(lastSpawnCmd).toEqual([
|
|
382
|
+
"wrangler",
|
|
383
|
+
"queues",
|
|
384
|
+
"create",
|
|
385
|
+
"my-queue",
|
|
386
|
+
]);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("queueDelete calls wrangler queues delete", async () => {
|
|
390
|
+
mockSpawnWithCapture(successSpawn("Deleted queue"));
|
|
391
|
+
|
|
392
|
+
const service = new CloudflareService();
|
|
393
|
+
const result = await service.queueDelete("old-queue");
|
|
394
|
+
|
|
395
|
+
expect(result.ok).toBe(true);
|
|
396
|
+
expect(lastSpawnCmd).toEqual([
|
|
397
|
+
"wrangler",
|
|
398
|
+
"queues",
|
|
399
|
+
"delete",
|
|
400
|
+
"old-queue",
|
|
401
|
+
]);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// -- Vectorize ------------------------------------------------------------
|
|
406
|
+
|
|
407
|
+
describe("vectorize operations", () => {
|
|
408
|
+
it("vectorizeList calls wrangler vectorize list --json", async () => {
|
|
409
|
+
mockSpawnWithCapture(successSpawn("[]"));
|
|
410
|
+
|
|
411
|
+
const service = new CloudflareService();
|
|
412
|
+
const result = await service.vectorizeList();
|
|
413
|
+
|
|
414
|
+
expect(result.ok).toBe(true);
|
|
415
|
+
expect(lastSpawnCmd).toEqual(["wrangler", "vectorize", "list", "--json"]);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it("vectorizeCreate calls wrangler vectorize create with defaults", async () => {
|
|
419
|
+
mockSpawnWithCapture(successSpawn("Created index my-index"));
|
|
420
|
+
|
|
421
|
+
const service = new CloudflareService();
|
|
422
|
+
const result = await service.vectorizeCreate("my-index");
|
|
423
|
+
|
|
424
|
+
expect(result.ok).toBe(true);
|
|
425
|
+
expect(lastSpawnCmd).toEqual([
|
|
426
|
+
"wrangler",
|
|
427
|
+
"vectorize",
|
|
428
|
+
"create",
|
|
429
|
+
"my-index",
|
|
430
|
+
"--dimensions",
|
|
431
|
+
"768",
|
|
432
|
+
"--metric",
|
|
433
|
+
"cosine",
|
|
434
|
+
]);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("vectorizeCreate passes custom dimensions and metric", async () => {
|
|
438
|
+
mockSpawnWithCapture(successSpawn("Created index my-index"));
|
|
439
|
+
|
|
440
|
+
const service = new CloudflareService();
|
|
441
|
+
const result = await service.vectorizeCreate("my-index", 384, "euclidean");
|
|
442
|
+
|
|
443
|
+
expect(result.ok).toBe(true);
|
|
444
|
+
expect(lastSpawnCmd).toEqual([
|
|
445
|
+
"wrangler",
|
|
446
|
+
"vectorize",
|
|
447
|
+
"create",
|
|
448
|
+
"my-index",
|
|
449
|
+
"--dimensions",
|
|
450
|
+
"384",
|
|
451
|
+
"--metric",
|
|
452
|
+
"euclidean",
|
|
453
|
+
]);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it("vectorizeDelete calls wrangler vectorize delete", async () => {
|
|
457
|
+
mockSpawnWithCapture(successSpawn("Deleted index"));
|
|
458
|
+
|
|
459
|
+
const service = new CloudflareService();
|
|
460
|
+
const result = await service.vectorizeDelete("old-index");
|
|
461
|
+
|
|
462
|
+
expect(result.ok).toBe(true);
|
|
463
|
+
expect(lastSpawnCmd).toEqual([
|
|
464
|
+
"wrangler",
|
|
465
|
+
"vectorize",
|
|
466
|
+
"delete",
|
|
467
|
+
"old-index",
|
|
468
|
+
]);
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// -- Analytics ------------------------------------------------------------
|
|
473
|
+
|
|
474
|
+
describe("analytics operations", () => {
|
|
475
|
+
it("analyticsList calls wrangler analytics dataset list", async () => {
|
|
476
|
+
mockSpawnWithCapture(successSpawn("[]"));
|
|
477
|
+
|
|
478
|
+
const service = new CloudflareService();
|
|
479
|
+
const result = await service.analyticsList();
|
|
480
|
+
|
|
481
|
+
expect(result.ok).toBe(true);
|
|
482
|
+
expect(lastSpawnCmd).toEqual([
|
|
483
|
+
"wrangler",
|
|
484
|
+
"analytics",
|
|
485
|
+
"dataset",
|
|
486
|
+
"list",
|
|
487
|
+
]);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("analyticsCreate returns error with Dashboard URL instructions", async () => {
|
|
491
|
+
const service = new CloudflareService();
|
|
492
|
+
const result = await service.analyticsCreate("my-dataset");
|
|
493
|
+
|
|
494
|
+
expect(result.ok).toBe(false);
|
|
495
|
+
if (!result.ok) {
|
|
496
|
+
expect(result.error).toContain("cannot be created via wrangler");
|
|
497
|
+
expect(result.error).toContain("dash.cloudflare.com");
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// -- Secrets --------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
describe("secret operations", () => {
|
|
505
|
+
it("secretList calls wrangler secret list --name", async () => {
|
|
506
|
+
mockSpawnWithCapture(successSpawn("[]"));
|
|
507
|
+
|
|
508
|
+
const service = new CloudflareService();
|
|
509
|
+
const result = await service.secretList("my-worker");
|
|
510
|
+
|
|
511
|
+
expect(result.ok).toBe(true);
|
|
512
|
+
expect(lastSpawnCmd).toEqual([
|
|
513
|
+
"wrangler",
|
|
514
|
+
"secret",
|
|
515
|
+
"list",
|
|
516
|
+
"--name",
|
|
517
|
+
"my-worker",
|
|
518
|
+
]);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("secretPut pipes value through stdin (never via CLI args)", async () => {
|
|
522
|
+
let stdinWritten = "";
|
|
523
|
+
let stdinEnded = false;
|
|
524
|
+
|
|
525
|
+
const spawnResult = {
|
|
526
|
+
stdout: new Blob(["Secret set successfully"]),
|
|
527
|
+
stderr: new Blob([""]),
|
|
528
|
+
exited: Promise.resolve(0),
|
|
529
|
+
stdin: {
|
|
530
|
+
write: mock((data: string) => {
|
|
531
|
+
stdinWritten = data;
|
|
532
|
+
}),
|
|
533
|
+
end: mock(() => {
|
|
534
|
+
stdinEnded = true;
|
|
535
|
+
}),
|
|
536
|
+
},
|
|
537
|
+
kill: mock(() => {}),
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
const spawnMock = mock((cmd: string[]) => {
|
|
541
|
+
lastSpawnCmd = cmd;
|
|
542
|
+
return spawnResult;
|
|
543
|
+
});
|
|
544
|
+
(Bun as unknown as Record<string, unknown>).spawn = spawnMock;
|
|
545
|
+
|
|
546
|
+
const service = new CloudflareService();
|
|
547
|
+
const result = await service.secretPut(
|
|
548
|
+
"my-worker",
|
|
549
|
+
"API_KEY",
|
|
550
|
+
"super-secret-value"
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
expect(result.ok).toBe(true);
|
|
554
|
+
// The secret value should NEVER appear in CLI args
|
|
555
|
+
const argsJoined = lastSpawnCmd.join(" ");
|
|
556
|
+
expect(argsJoined).not.toContain("super-secret-value");
|
|
557
|
+
// stdin should receive the secret value
|
|
558
|
+
expect(stdinWritten).toBe("super-secret-value\n");
|
|
559
|
+
expect(stdinEnded).toBe(true);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it("secretDelete calls wrangler secret delete", async () => {
|
|
563
|
+
mockSpawnWithCapture(successSpawn("Deleted secret"));
|
|
564
|
+
|
|
565
|
+
const service = new CloudflareService();
|
|
566
|
+
const result = await service.secretDelete("my-worker", "OLD_KEY");
|
|
567
|
+
|
|
568
|
+
expect(result.ok).toBe(true);
|
|
569
|
+
expect(lastSpawnCmd).toEqual([
|
|
570
|
+
"wrangler",
|
|
571
|
+
"secret",
|
|
572
|
+
"delete",
|
|
573
|
+
"OLD_KEY",
|
|
574
|
+
"--name",
|
|
575
|
+
"my-worker",
|
|
576
|
+
]);
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// -- zonesList ------------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
describe("zonesList", () => {
|
|
583
|
+
it("calls wrangler zones list", async () => {
|
|
584
|
+
mockSpawnWithCapture(successSpawn("[]"));
|
|
585
|
+
|
|
586
|
+
const service = new CloudflareService();
|
|
587
|
+
const result = await service.zonesList();
|
|
588
|
+
|
|
589
|
+
expect(result.ok).toBe(true);
|
|
590
|
+
expect(lastSpawnCmd).toEqual(["wrangler", "zones", "list"]);
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// -- Error handling -------------------------------------------------------
|
|
595
|
+
|
|
596
|
+
describe("error handling", () => {
|
|
597
|
+
it("returns ok:false when wrangler exits non-zero", async () => {
|
|
598
|
+
mockSpawnWithCapture(errorSpawn("fatal error: something broke", 2));
|
|
599
|
+
|
|
600
|
+
const service = new CloudflareService();
|
|
601
|
+
const result = await service.whoami();
|
|
602
|
+
|
|
603
|
+
expect(result.ok).toBe(false);
|
|
604
|
+
if (!result.ok) {
|
|
605
|
+
expect(result.error).toBe("fatal error: something broke");
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("falls back to exit code message when stderr is empty", async () => {
|
|
610
|
+
mockSpawnWithCapture(makeSpawnResult("", "", 1));
|
|
611
|
+
|
|
612
|
+
const service = new CloudflareService();
|
|
613
|
+
const result = await service.whoami();
|
|
614
|
+
|
|
615
|
+
expect(result.ok).toBe(false);
|
|
616
|
+
if (!result.ok) {
|
|
617
|
+
expect(result.error).toBe("wrangler exited with code 1");
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("returns error when Bun.spawn itself throws", async () => {
|
|
622
|
+
// Simulate spawn throwing (e.g. wrangler not installed)
|
|
623
|
+
const spawnMock = mock(() => {
|
|
624
|
+
throw new Error("ENOENT: wrangler not found");
|
|
625
|
+
});
|
|
626
|
+
(Bun as unknown as Record<string, unknown>).spawn = spawnMock;
|
|
627
|
+
|
|
628
|
+
const service = new CloudflareService();
|
|
629
|
+
const result = await service.whoami();
|
|
630
|
+
|
|
631
|
+
expect(result.ok).toBe(false);
|
|
632
|
+
if (!result.ok) {
|
|
633
|
+
expect(result.error).toContain("Failed to spawn wrangler");
|
|
634
|
+
expect(result.error).toContain("ENOENT");
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
// -- Stdout parsing -------------------------------------------------------
|
|
640
|
+
|
|
641
|
+
describe("stdout parsing", () => {
|
|
642
|
+
it("trims trailing whitespace from stdout", async () => {
|
|
643
|
+
mockSpawnWithCapture(successSpawn(" hello world \n"));
|
|
644
|
+
|
|
645
|
+
const service = new CloudflareService();
|
|
646
|
+
const result = await service.whoami();
|
|
647
|
+
|
|
648
|
+
expect(result.ok).toBe(true);
|
|
649
|
+
if (result.ok) {
|
|
650
|
+
expect(result.data).toBe("hello world");
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
});
|