@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,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `hoox waf` command.
|
|
3
|
+
*
|
|
4
|
+
* Mocks CloudflareService.zonesList() and global fetch() for Cloudflare API
|
|
5
|
+
* calls. Tests verify command action functions produce correct output and
|
|
6
|
+
* handle errors gracefully.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
afterEach,
|
|
11
|
+
beforeEach,
|
|
12
|
+
describe,
|
|
13
|
+
expect,
|
|
14
|
+
it,
|
|
15
|
+
mock,
|
|
16
|
+
spyOn,
|
|
17
|
+
} from "bun:test";
|
|
18
|
+
import { Command } from "commander";
|
|
19
|
+
import { registerWafCommand } from "./waf-command.js";
|
|
20
|
+
import { CloudflareService } from "../../services/cloudflare/cloudflare-service.js";
|
|
21
|
+
import type { WranglerResult } from "../../services/cloudflare/types.js";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
const realFetch = globalThis.fetch;
|
|
28
|
+
const realEnv = { ...process.env };
|
|
29
|
+
|
|
30
|
+
/** Mock CloudflareService.zonesList to return a known zone. */
|
|
31
|
+
function mockZonesList(output: string, ok = true, error?: string): void {
|
|
32
|
+
const result: WranglerResult<string> = ok
|
|
33
|
+
? { ok: true, data: output }
|
|
34
|
+
: { ok: false, error: error ?? "zone list failed" };
|
|
35
|
+
|
|
36
|
+
spyOn(CloudflareService.prototype, "zonesList").mockImplementation(() =>
|
|
37
|
+
Promise.resolve(result)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sets up a mock fetch that returns a CF-style JSON response.
|
|
43
|
+
* Returns the list of calls made to fetch for assertions.
|
|
44
|
+
*/
|
|
45
|
+
function mockCfFetch(
|
|
46
|
+
responseMap: Record<string, unknown>
|
|
47
|
+
): Array<{ url: string; method: string; body?: string }> {
|
|
48
|
+
const calls: Array<{ url: string; method: string; body?: string }> = [];
|
|
49
|
+
|
|
50
|
+
const fetchMock = mock(
|
|
51
|
+
async (url: string, init?: Record<string, unknown>) => {
|
|
52
|
+
void init; // Mark as used
|
|
53
|
+
// Find matching response by path partial match
|
|
54
|
+
let matched: unknown = null;
|
|
55
|
+
for (const [pathPattern, response] of Object.entries(responseMap)) {
|
|
56
|
+
if ((url as string).includes(pathPattern)) {
|
|
57
|
+
matched = response;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
calls.push({
|
|
63
|
+
url: url as string,
|
|
64
|
+
method: (init?.method ?? "GET") as string,
|
|
65
|
+
body: init?.body as string | undefined,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (
|
|
69
|
+
matched &&
|
|
70
|
+
typeof matched === "object" &&
|
|
71
|
+
matched !== null &&
|
|
72
|
+
"error" in (matched as Record<string, unknown>)
|
|
73
|
+
) {
|
|
74
|
+
// Error response
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
status: 400,
|
|
78
|
+
json: async () => ({
|
|
79
|
+
success: false,
|
|
80
|
+
errors: [{ message: (matched as Record<string, string>).error }],
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
status: 200,
|
|
88
|
+
json: async () => ({
|
|
89
|
+
success: true,
|
|
90
|
+
result: matched,
|
|
91
|
+
}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
(globalThis as unknown as Record<string, unknown>).fetch = fetchMock;
|
|
97
|
+
return calls;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Setup / Teardown
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
// Set required env vars for CF API calls
|
|
106
|
+
process.env.CLOUDFLARE_API_TOKEN = "test-token";
|
|
107
|
+
process.env.CLOUDFLARE_ACCOUNT_ID = "test-account-id";
|
|
108
|
+
process.env.CLOUDFLARE_ZONE_ID = ""; // force zonesList path
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
(globalThis as unknown as Record<string, unknown>).fetch = realFetch;
|
|
113
|
+
process.env = { ...realEnv };
|
|
114
|
+
mock.restore();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Helper: build a Commander program with waf registered
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
function makeProgram(): Command {
|
|
122
|
+
const program = new Command()
|
|
123
|
+
.name("hoox")
|
|
124
|
+
.option("--json", "JSON output")
|
|
125
|
+
.option("--quiet", "Minimal output");
|
|
126
|
+
|
|
127
|
+
registerWafCommand(program);
|
|
128
|
+
return program;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Run a command string (e.g. "waf status --json") and capture stdout. */
|
|
132
|
+
async function runCommand(
|
|
133
|
+
args: string
|
|
134
|
+
): Promise<{ stdout: string; exitCode: number }> {
|
|
135
|
+
const program = makeProgram();
|
|
136
|
+
|
|
137
|
+
let stdout = "";
|
|
138
|
+
const writeSpy = spyOn(process.stdout, "write").mockImplementation(
|
|
139
|
+
(chunk: unknown) => {
|
|
140
|
+
stdout += typeof chunk === "string" ? chunk : String(chunk ?? "");
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// We don't want process.exit() to kill the test runner
|
|
146
|
+
const exitSpy = spyOn(process, "exit").mockImplementation(
|
|
147
|
+
(() => {}) as never
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
await program.parseAsync([...args.split(" ")], { from: "user" });
|
|
152
|
+
} catch {
|
|
153
|
+
// Some commands may fail — that's fine, we test output
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
writeSpy.mockRestore();
|
|
157
|
+
exitSpy.mockRestore();
|
|
158
|
+
|
|
159
|
+
return { stdout, exitCode: 0 };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Tests
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
describe("waf command", () => {
|
|
167
|
+
// -- waf status ------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
describe("status", () => {
|
|
170
|
+
it("shows WAF status when enabled", async () => {
|
|
171
|
+
mockZonesList("example.com (abc123def456)");
|
|
172
|
+
mockCfFetch({
|
|
173
|
+
"/settings/waf": { id: "waf", value: "on" },
|
|
174
|
+
"/firewall/rules": [
|
|
175
|
+
{
|
|
176
|
+
id: "rule-1",
|
|
177
|
+
description: "Block bad IPs",
|
|
178
|
+
action: "block",
|
|
179
|
+
filter: { id: "f1", expression: "(ip.src eq 1.2.3.4)" },
|
|
180
|
+
created_on: "2026-01-01T00:00:00Z",
|
|
181
|
+
modified_on: "2026-01-02T00:00:00Z",
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
"/analytics/dashboard": { totals: { threats: 42 } },
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const { stdout } = await runCommand("waf status");
|
|
188
|
+
|
|
189
|
+
expect(stdout).toContain("example.com");
|
|
190
|
+
expect(stdout).toContain("ENABLED");
|
|
191
|
+
expect(stdout).toContain("1 active");
|
|
192
|
+
expect(stdout).toContain("42");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("shows WAF status when disabled", async () => {
|
|
196
|
+
mockZonesList("example.com (abc123def456)");
|
|
197
|
+
mockCfFetch({
|
|
198
|
+
"/settings/waf": { id: "waf", value: "off" },
|
|
199
|
+
"/firewall/rules": [],
|
|
200
|
+
"/analytics/dashboard": { totals: { threats: 0 } },
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const { stdout } = await runCommand("waf status");
|
|
204
|
+
|
|
205
|
+
expect(stdout).toContain("DISABLED");
|
|
206
|
+
expect(stdout).toContain("0 active");
|
|
207
|
+
expect(stdout).toContain("0");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("outputs JSON when --json flag is used", async () => {
|
|
211
|
+
mockZonesList("example.com (abc123def456)");
|
|
212
|
+
mockCfFetch({
|
|
213
|
+
"/settings/waf": { id: "waf", value: "on" },
|
|
214
|
+
"/firewall/rules": [],
|
|
215
|
+
"/analytics/dashboard": { totals: { threats: 0 } },
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const { stdout } = await runCommand("waf status --json");
|
|
219
|
+
|
|
220
|
+
const parsed = JSON.parse(stdout.trim());
|
|
221
|
+
expect(parsed).toHaveProperty("enabled", true);
|
|
222
|
+
expect(parsed).toHaveProperty("zoneId", "abc123def456");
|
|
223
|
+
expect(parsed).toHaveProperty("zoneName", "example.com");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("throws when zonesList fails", async () => {
|
|
227
|
+
mockZonesList("", false, "auth error");
|
|
228
|
+
|
|
229
|
+
const { stdout } = await runCommand("waf status");
|
|
230
|
+
|
|
231
|
+
expect(stdout).toContain("Failed to list zones");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("uses CLOUDFLARE_ZONE_ID env var when set", async () => {
|
|
235
|
+
process.env.CLOUDFLARE_ZONE_ID = "direct-zone-id";
|
|
236
|
+
mockCfFetch({
|
|
237
|
+
"/settings/waf": { id: "waf", value: "on" },
|
|
238
|
+
"/firewall/rules": [],
|
|
239
|
+
"/analytics/dashboard": { totals: { threats: 0 } },
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const { stdout } = await runCommand("waf status");
|
|
243
|
+
|
|
244
|
+
expect(stdout).toContain("direct-zone-id");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// -- waf rules list --------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
describe("rules list", () => {
|
|
251
|
+
it("lists firewall rules in human-readable format", async () => {
|
|
252
|
+
mockZonesList("example.com (zone-1)");
|
|
253
|
+
mockCfFetch({
|
|
254
|
+
"/firewall/rules": [
|
|
255
|
+
{
|
|
256
|
+
id: "rule-1-is-a-very-long-id",
|
|
257
|
+
description: "IP blocklist rule",
|
|
258
|
+
action: "block",
|
|
259
|
+
filter: {
|
|
260
|
+
id: "f1",
|
|
261
|
+
expression: "(ip.src eq 10.0.0.1)",
|
|
262
|
+
},
|
|
263
|
+
created_on: "2026-01-01T00:00:00Z",
|
|
264
|
+
modified_on: "2026-01-02T00:00:00Z",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: "rule-2-short",
|
|
268
|
+
description: "",
|
|
269
|
+
action: "allow",
|
|
270
|
+
filter: {
|
|
271
|
+
id: "f2",
|
|
272
|
+
expression: '(http.request.uri.path contains "/api")',
|
|
273
|
+
},
|
|
274
|
+
created_on: "2026-01-03T00:00:00Z",
|
|
275
|
+
modified_on: "2026-01-04T00:00:00Z",
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const { stdout } = await runCommand("waf rules list");
|
|
281
|
+
|
|
282
|
+
expect(stdout).toContain("IP blocklist rule");
|
|
283
|
+
expect(stdout).toContain("block");
|
|
284
|
+
expect(stdout).toContain("allow");
|
|
285
|
+
expect(stdout).toContain("(ip.src eq 10.0.0.1)");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("outputs JSON when --json flag is used", async () => {
|
|
289
|
+
mockZonesList("example.com (zone-1)");
|
|
290
|
+
mockCfFetch({
|
|
291
|
+
"/firewall/rules": [
|
|
292
|
+
{
|
|
293
|
+
id: "rule-1",
|
|
294
|
+
description: "Test rule",
|
|
295
|
+
action: "block",
|
|
296
|
+
filter: { id: "f1", expression: "(ip.src eq 1.1.1.1)" },
|
|
297
|
+
created_on: "2026-01-01T00:00:00Z",
|
|
298
|
+
modified_on: "2026-01-01T00:00:00Z",
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const { stdout } = await runCommand("waf rules list --json");
|
|
304
|
+
|
|
305
|
+
const parsed = JSON.parse(stdout.trim());
|
|
306
|
+
expect(Array.isArray(parsed)).toBe(true);
|
|
307
|
+
expect(parsed[0]).toHaveProperty("id", "rule-1");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("shows empty message when no rules exist", async () => {
|
|
311
|
+
mockZonesList("example.com (zone-1)");
|
|
312
|
+
mockCfFetch({ "/firewall/rules": [] });
|
|
313
|
+
|
|
314
|
+
const { stdout } = await runCommand("waf rules list");
|
|
315
|
+
|
|
316
|
+
expect(stdout).toContain("No firewall rules");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("throws when rules fetch fails", async () => {
|
|
320
|
+
mockZonesList("example.com (zone-1)");
|
|
321
|
+
mockCfFetch({
|
|
322
|
+
"/firewall/rules": { error: "API error" },
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const { stdout } = await runCommand("waf rules list");
|
|
326
|
+
|
|
327
|
+
expect(stdout).toContain("API error");
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// -- waf rules add ---------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
describe("rules add", () => {
|
|
334
|
+
it("adds an IP allowlist rule", async () => {
|
|
335
|
+
mockZonesList("example.com (zone-1)");
|
|
336
|
+
mockCfFetch({
|
|
337
|
+
"/filters": { id: "filter-1" },
|
|
338
|
+
// NOTE: the firewall/rules endpoint is matched but returns array
|
|
339
|
+
"/firewall/rules": [
|
|
340
|
+
{
|
|
341
|
+
id: "new-rule-1",
|
|
342
|
+
description: "ip-allowlist rule for 192.168.1.1",
|
|
343
|
+
action: "allow",
|
|
344
|
+
filter: { id: "filter-1", expression: "(ip.src eq 192.168.1.1)" },
|
|
345
|
+
created_on: "2026-01-01T00:00:00Z",
|
|
346
|
+
modified_on: "2026-01-01T00:00:00Z",
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const { stdout } = await runCommand(
|
|
352
|
+
"waf rules add ip-allowlist 192.168.1.1"
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
expect(stdout).toContain("WAF rule added");
|
|
356
|
+
expect(stdout).toContain("192.168.1.1");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("rejects invalid rule types", async () => {
|
|
360
|
+
mockZonesList("example.com (zone-1)");
|
|
361
|
+
|
|
362
|
+
const { stdout } = await runCommand("waf rules add invalid-type 1.2.3.4");
|
|
363
|
+
|
|
364
|
+
expect(stdout).toContain("Invalid rule type");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("throws when filter creation fails", async () => {
|
|
368
|
+
mockZonesList("example.com (zone-1)");
|
|
369
|
+
mockCfFetch({
|
|
370
|
+
"/filters": { error: "Filter creation failed" },
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const { stdout } = await runCommand(
|
|
374
|
+
"waf rules add ip-blocklist 10.0.0.1"
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
expect(stdout).toContain("Filter creation failed");
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// -- waf rules remove ------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
describe("rules remove", () => {
|
|
384
|
+
it("removes a WAF rule by ID", async () => {
|
|
385
|
+
mockZonesList("example.com (zone-1)");
|
|
386
|
+
mockCfFetch({
|
|
387
|
+
"/firewall/rules/rule-to-delete": { id: "rule-to-delete" },
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
const { stdout } = await runCommand("waf rules remove rule-to-delete");
|
|
391
|
+
|
|
392
|
+
expect(stdout).toContain("removed");
|
|
393
|
+
expect(stdout).toContain("rule-to-delete");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("throws when rule deletion fails", async () => {
|
|
397
|
+
mockZonesList("example.com (zone-1)");
|
|
398
|
+
mockCfFetch({
|
|
399
|
+
"/firewall/rules/bad-id": { error: "Rule not found" },
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const { stdout } = await runCommand("waf rules remove bad-id");
|
|
403
|
+
|
|
404
|
+
expect(stdout).toContain("Rule not found");
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// -- waf mode --------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
describe("mode", () => {
|
|
411
|
+
it("enables WAF on the zone", async () => {
|
|
412
|
+
mockZonesList("example.com (zone-1)");
|
|
413
|
+
mockCfFetch({
|
|
414
|
+
"/settings/waf": { id: "waf", value: "on" },
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const { stdout } = await runCommand("waf mode enable");
|
|
418
|
+
|
|
419
|
+
expect(stdout).toContain("enabled");
|
|
420
|
+
expect(stdout).toContain("example.com");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("disables WAF on the zone", async () => {
|
|
424
|
+
mockZonesList("example.com (zone-1)");
|
|
425
|
+
mockCfFetch({
|
|
426
|
+
"/settings/waf": { id: "waf", value: "off" },
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const { stdout } = await runCommand("waf mode disable");
|
|
430
|
+
|
|
431
|
+
expect(stdout).toContain("disabled");
|
|
432
|
+
expect(stdout).toContain("example.com");
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it("throws when mode update fails", async () => {
|
|
436
|
+
mockZonesList("example.com (zone-1)");
|
|
437
|
+
mockCfFetch({
|
|
438
|
+
"/settings/waf": { error: "Permission denied" },
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const { stdout } = await runCommand("waf mode enable");
|
|
442
|
+
|
|
443
|
+
expect(stdout).toContain("Permission denied");
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// -- Error handling --------------------------------------------------------
|
|
448
|
+
|
|
449
|
+
describe("error handling", () => {
|
|
450
|
+
it("throws when CLOUDFLARE_API_TOKEN is missing", async () => {
|
|
451
|
+
delete process.env.CLOUDFLARE_API_TOKEN;
|
|
452
|
+
mockZonesList("example.com (zone-1)");
|
|
453
|
+
|
|
454
|
+
const { stdout } = await runCommand("waf status");
|
|
455
|
+
|
|
456
|
+
expect(stdout).toContain("CLOUDFLARE_API_TOKEN");
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("throws when CLOUDFLARE_ACCOUNT_ID is missing", async () => {
|
|
460
|
+
delete process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
461
|
+
mockZonesList("example.com (zone-1)");
|
|
462
|
+
|
|
463
|
+
const { stdout } = await runCommand("waf status");
|
|
464
|
+
|
|
465
|
+
expect(stdout).toContain("CLOUDFLARE_ACCOUNT_ID");
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// -- Subcommand structure --------------------------------------------------
|
|
470
|
+
|
|
471
|
+
describe("subcommand structure", () => {
|
|
472
|
+
it("registers waf as a command", () => {
|
|
473
|
+
const program = makeProgram();
|
|
474
|
+
const wafCmd = program.commands.find((c) => c.name() === "waf");
|
|
475
|
+
expect(wafCmd).toBeDefined();
|
|
476
|
+
|
|
477
|
+
const subNames = (wafCmd as Command).commands.map((c) => c.name());
|
|
478
|
+
expect(subNames).toContain("status");
|
|
479
|
+
expect(subNames).toContain("rules");
|
|
480
|
+
expect(subNames).toContain("mode");
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("registers rules subcommands", () => {
|
|
484
|
+
const program = makeProgram();
|
|
485
|
+
const wafCmd = program.commands.find((c) => c.name() === "waf")!;
|
|
486
|
+
const rulesCmd = (wafCmd as Command).commands.find(
|
|
487
|
+
(c) => c.name() === "rules"
|
|
488
|
+
)!;
|
|
489
|
+
const ruleSubNames = (rulesCmd as Command).commands.map((c) => c.name());
|
|
490
|
+
expect(ruleSubNames).toContain("list");
|
|
491
|
+
expect(ruleSubNames).toContain("add");
|
|
492
|
+
expect(ruleSubNames).toContain("remove");
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("registers mode subcommands", () => {
|
|
496
|
+
const program = makeProgram();
|
|
497
|
+
const wafCmd = program.commands.find((c) => c.name() === "waf")!;
|
|
498
|
+
const modeCmd = (wafCmd as Command).commands.find(
|
|
499
|
+
(c) => c.name() === "mode"
|
|
500
|
+
)!;
|
|
501
|
+
const modeSubNames = (modeCmd as Command).commands.map((c) => c.name());
|
|
502
|
+
expect(modeSubNames).toContain("enable");
|
|
503
|
+
expect(modeSubNames).toContain("disable");
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
});
|