@bryan-thompson/inspector-assessment-cli 1.25.9 → 1.26.0
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,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Flag Parsing Unit Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for validation functions used in CLI argument parsing:
|
|
5
|
+
* - parseKeyValuePair() - KEY=VALUE parsing for env vars
|
|
6
|
+
* - parseHeaderPair() - "HeaderName: Value" parsing
|
|
7
|
+
* - validateEnvVars() - Env var name/value validation and sensitive var blocking
|
|
8
|
+
* - validateServerUrl() - SSRF protection and URL validation
|
|
9
|
+
* - validateCommand() - Command injection prevention
|
|
10
|
+
* - validateModuleNames() - Module name validation
|
|
11
|
+
* - Profile/format validation - CLI option validation
|
|
12
|
+
* - Mutual exclusivity - Flag conflict detection
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect } from "@jest/globals";
|
|
15
|
+
describe("Key-Value Pair Parsing", () => {
|
|
16
|
+
/**
|
|
17
|
+
* Recreates parseKeyValuePair logic from cli.ts
|
|
18
|
+
*/
|
|
19
|
+
function parseKeyValuePair(value, previous = {}) {
|
|
20
|
+
const parts = value.split("=");
|
|
21
|
+
const key = parts[0];
|
|
22
|
+
const val = parts.slice(1).join("=");
|
|
23
|
+
if (val === undefined || val === "") {
|
|
24
|
+
throw new Error(`Invalid parameter format: ${value}. Use key=value format.`);
|
|
25
|
+
}
|
|
26
|
+
return { ...previous, [key]: val };
|
|
27
|
+
}
|
|
28
|
+
describe("Valid key-value pairs", () => {
|
|
29
|
+
it("should parse simple KEY=VALUE format", () => {
|
|
30
|
+
expect(parseKeyValuePair("MY_VAR=hello")).toEqual({ MY_VAR: "hello" });
|
|
31
|
+
});
|
|
32
|
+
it("should handle multiple equals signs in value", () => {
|
|
33
|
+
expect(parseKeyValuePair("KEY=val=ue=test")).toEqual({
|
|
34
|
+
KEY: "val=ue=test",
|
|
35
|
+
});
|
|
36
|
+
expect(parseKeyValuePair("URL=https://example.com?a=1&b=2")).toEqual({
|
|
37
|
+
URL: "https://example.com?a=1&b=2",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it("should accumulate multiple pairs", () => {
|
|
41
|
+
let result = parseKeyValuePair("VAR1=value1");
|
|
42
|
+
result = parseKeyValuePair("VAR2=value2", result);
|
|
43
|
+
expect(result).toEqual({ VAR1: "value1", VAR2: "value2" });
|
|
44
|
+
});
|
|
45
|
+
it("should handle special characters in value", () => {
|
|
46
|
+
expect(parseKeyValuePair("PATH=/usr/bin:/usr/local/bin")).toEqual({
|
|
47
|
+
PATH: "/usr/bin:/usr/local/bin",
|
|
48
|
+
});
|
|
49
|
+
expect(parseKeyValuePair('JSON={"key":"value"}')).toEqual({
|
|
50
|
+
JSON: '{"key":"value"}',
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("Invalid key-value pairs", () => {
|
|
55
|
+
it("should throw on missing equals sign", () => {
|
|
56
|
+
expect(() => parseKeyValuePair("NOEQUALS")).toThrow("Invalid parameter format");
|
|
57
|
+
});
|
|
58
|
+
it("should throw on empty value", () => {
|
|
59
|
+
expect(() => parseKeyValuePair("KEY=")).toThrow("Invalid parameter format");
|
|
60
|
+
});
|
|
61
|
+
it("should throw on key without equals", () => {
|
|
62
|
+
expect(() => parseKeyValuePair("KEY")).toThrow("Invalid parameter format");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe("Header Pair Parsing", () => {
|
|
67
|
+
/**
|
|
68
|
+
* Recreates parseHeaderPair logic from cli.ts
|
|
69
|
+
*/
|
|
70
|
+
function parseHeaderPair(value, previous = {}) {
|
|
71
|
+
const colonIndex = value.indexOf(":");
|
|
72
|
+
if (colonIndex === -1) {
|
|
73
|
+
throw new Error(`Invalid header format: ${value}. Use "HeaderName: Value" format.`);
|
|
74
|
+
}
|
|
75
|
+
const key = value.slice(0, colonIndex).trim();
|
|
76
|
+
const val = value.slice(colonIndex + 1).trim();
|
|
77
|
+
if (key === "" || val === "") {
|
|
78
|
+
throw new Error(`Invalid header format: ${value}. Use "HeaderName: Value" format.`);
|
|
79
|
+
}
|
|
80
|
+
return { ...previous, [key]: val };
|
|
81
|
+
}
|
|
82
|
+
describe("Valid header pairs", () => {
|
|
83
|
+
it("should parse simple HeaderName: Value format", () => {
|
|
84
|
+
expect(parseHeaderPair("Authorization: Bearer token123")).toEqual({
|
|
85
|
+
Authorization: "Bearer token123",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
it("should handle colons in value", () => {
|
|
89
|
+
expect(parseHeaderPair("X-Custom: https://example.com:8080")).toEqual({
|
|
90
|
+
"X-Custom": "https://example.com:8080",
|
|
91
|
+
});
|
|
92
|
+
expect(parseHeaderPair("Time: 12:30:45")).toEqual({ Time: "12:30:45" });
|
|
93
|
+
});
|
|
94
|
+
it("should trim whitespace", () => {
|
|
95
|
+
expect(parseHeaderPair(" Content-Type : application/json ")).toEqual({
|
|
96
|
+
"Content-Type": "application/json",
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it("should accumulate multiple headers", () => {
|
|
100
|
+
let result = parseHeaderPair("Accept: application/json");
|
|
101
|
+
result = parseHeaderPair("User-Agent: inspector-cli/1.0", result);
|
|
102
|
+
expect(result).toEqual({
|
|
103
|
+
Accept: "application/json",
|
|
104
|
+
"User-Agent": "inspector-cli/1.0",
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe("Invalid header pairs", () => {
|
|
109
|
+
it("should throw on missing colon", () => {
|
|
110
|
+
expect(() => parseHeaderPair("Authorization Bearer token")).toThrow("Invalid header format");
|
|
111
|
+
});
|
|
112
|
+
it("should throw on empty header name", () => {
|
|
113
|
+
expect(() => parseHeaderPair(": value")).toThrow("Invalid header format");
|
|
114
|
+
});
|
|
115
|
+
it("should throw on empty header value", () => {
|
|
116
|
+
expect(() => parseHeaderPair("Header:")).toThrow("Invalid header format");
|
|
117
|
+
expect(() => parseHeaderPair("Header: ")).toThrow("Invalid header format");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe("Environment Variable Validation", () => {
|
|
122
|
+
/**
|
|
123
|
+
* Recreates validation logic from cli.ts
|
|
124
|
+
*/
|
|
125
|
+
function isValidEnvVarName(name) {
|
|
126
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
|
|
127
|
+
}
|
|
128
|
+
function isValidEnvVarValue(value) {
|
|
129
|
+
return !value.includes("\0");
|
|
130
|
+
}
|
|
131
|
+
const BLOCKED_ENV_VAR_PATTERNS = [
|
|
132
|
+
/^(AWS|AZURE|GCP|GOOGLE)_/i,
|
|
133
|
+
/^(API|AUTH|SECRET|TOKEN|KEY|PASSWORD|CREDENTIAL)_/i,
|
|
134
|
+
/^(PRIVATE|SSH|PGP|GPG)_/i,
|
|
135
|
+
/_(API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL)$/i,
|
|
136
|
+
];
|
|
137
|
+
function isSensitiveEnvVar(name) {
|
|
138
|
+
return BLOCKED_ENV_VAR_PATTERNS.some((pattern) => pattern.test(name));
|
|
139
|
+
}
|
|
140
|
+
describe("Valid environment variable names", () => {
|
|
141
|
+
it("should accept valid names", () => {
|
|
142
|
+
expect(isValidEnvVarName("MY_VAR")).toBe(true);
|
|
143
|
+
expect(isValidEnvVarName("PATH")).toBe(true);
|
|
144
|
+
expect(isValidEnvVarName("_INTERNAL")).toBe(true);
|
|
145
|
+
expect(isValidEnvVarName("VAR_123")).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
it("should reject names starting with numbers", () => {
|
|
148
|
+
expect(isValidEnvVarName("123VAR")).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
it("should reject names with special characters", () => {
|
|
151
|
+
expect(isValidEnvVarName("MY-VAR")).toBe(false);
|
|
152
|
+
expect(isValidEnvVarName("MY.VAR")).toBe(false);
|
|
153
|
+
expect(isValidEnvVarName("MY VAR")).toBe(false);
|
|
154
|
+
expect(isValidEnvVarName("MY@VAR")).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
it("should reject empty names", () => {
|
|
157
|
+
expect(isValidEnvVarName("")).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe("Valid environment variable values", () => {
|
|
161
|
+
it("should accept normal values", () => {
|
|
162
|
+
expect(isValidEnvVarValue("hello world")).toBe(true);
|
|
163
|
+
expect(isValidEnvVarValue("/path/to/file")).toBe(true);
|
|
164
|
+
expect(isValidEnvVarValue("")).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
it("should reject values with null bytes", () => {
|
|
167
|
+
expect(isValidEnvVarValue("hello\0world")).toBe(false);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
describe("Sensitive environment variable detection", () => {
|
|
171
|
+
it("should block AWS credentials", () => {
|
|
172
|
+
expect(isSensitiveEnvVar("AWS_ACCESS_KEY_ID")).toBe(true);
|
|
173
|
+
expect(isSensitiveEnvVar("AWS_SECRET_ACCESS_KEY")).toBe(true);
|
|
174
|
+
expect(isSensitiveEnvVar("aws_session_token")).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
it("should block Azure credentials", () => {
|
|
177
|
+
expect(isSensitiveEnvVar("AZURE_CLIENT_SECRET")).toBe(true);
|
|
178
|
+
expect(isSensitiveEnvVar("AZURE_SUBSCRIPTION_ID")).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
it("should block GCP credentials", () => {
|
|
181
|
+
expect(isSensitiveEnvVar("GOOGLE_APPLICATION_CREDENTIALS")).toBe(true);
|
|
182
|
+
expect(isSensitiveEnvVar("GCP_SERVICE_ACCOUNT_KEY")).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
it("should block generic secrets", () => {
|
|
185
|
+
expect(isSensitiveEnvVar("API_KEY")).toBe(true);
|
|
186
|
+
expect(isSensitiveEnvVar("AUTH_TOKEN")).toBe(true);
|
|
187
|
+
expect(isSensitiveEnvVar("SECRET_KEY")).toBe(true);
|
|
188
|
+
expect(isSensitiveEnvVar("PASSWORD_HASH")).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
it("should block suffix patterns", () => {
|
|
191
|
+
expect(isSensitiveEnvVar("GITHUB_API_KEY")).toBe(true);
|
|
192
|
+
expect(isSensitiveEnvVar("DATABASE_PASSWORD")).toBe(true);
|
|
193
|
+
expect(isSensitiveEnvVar("OAUTH_TOKEN")).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
it("should block private keys", () => {
|
|
196
|
+
expect(isSensitiveEnvVar("PRIVATE_KEY")).toBe(true);
|
|
197
|
+
expect(isSensitiveEnvVar("SSH_KEY_PATH")).toBe(true);
|
|
198
|
+
expect(isSensitiveEnvVar("GPG_PASSPHRASE")).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
it("should allow safe environment variables", () => {
|
|
201
|
+
expect(isSensitiveEnvVar("NODE_ENV")).toBe(false);
|
|
202
|
+
expect(isSensitiveEnvVar("PATH")).toBe(false);
|
|
203
|
+
expect(isSensitiveEnvVar("HOME")).toBe(false);
|
|
204
|
+
expect(isSensitiveEnvVar("LOG_LEVEL")).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
describe("URL Validation (SSRF Protection)", () => {
|
|
209
|
+
/**
|
|
210
|
+
* Recreates SSRF validation logic from cli.ts
|
|
211
|
+
*/
|
|
212
|
+
const PRIVATE_HOSTNAME_PATTERNS = [
|
|
213
|
+
/^localhost$/,
|
|
214
|
+
/^localhost\./,
|
|
215
|
+
/^127\./,
|
|
216
|
+
/^10\./,
|
|
217
|
+
/^172\.(1[6-9]|2[0-9]|3[01])\./,
|
|
218
|
+
/^192\.168\./,
|
|
219
|
+
/^169\.254\./,
|
|
220
|
+
/^0\./,
|
|
221
|
+
/^\[::1\]$/,
|
|
222
|
+
/^\[::ffff:127\./,
|
|
223
|
+
/^\[fe80:/i,
|
|
224
|
+
/^\[fc/i,
|
|
225
|
+
/^\[fd/i,
|
|
226
|
+
/^169\.254\.169\.254$/,
|
|
227
|
+
/^metadata\./,
|
|
228
|
+
];
|
|
229
|
+
function isPrivateHostname(hostname) {
|
|
230
|
+
const normalizedHostname = hostname.toLowerCase();
|
|
231
|
+
return PRIVATE_HOSTNAME_PATTERNS.some((pattern) => pattern.test(normalizedHostname));
|
|
232
|
+
}
|
|
233
|
+
function validateServerUrl(url) {
|
|
234
|
+
try {
|
|
235
|
+
const parsed = new URL(url);
|
|
236
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
237
|
+
return {
|
|
238
|
+
valid: false,
|
|
239
|
+
error: `Invalid URL protocol: ${parsed.protocol}. Must be http or https.`,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// In production, private hostnames only warn (don't error)
|
|
243
|
+
// For testing purposes, we'll track if it's private
|
|
244
|
+
const isPrivate = isPrivateHostname(parsed.hostname);
|
|
245
|
+
return { valid: true, error: isPrivate ? "PRIVATE_WARNING" : undefined };
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
return { valid: false, error: "Invalid URL format" };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
describe("Valid HTTP/HTTPS URLs", () => {
|
|
252
|
+
it("should accept HTTP URLs", () => {
|
|
253
|
+
expect(validateServerUrl("http://example.com")).toEqual({ valid: true });
|
|
254
|
+
expect(validateServerUrl("http://api.example.com:8080/mcp")).toEqual({
|
|
255
|
+
valid: true,
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
it("should accept HTTPS URLs", () => {
|
|
259
|
+
expect(validateServerUrl("https://example.com")).toEqual({ valid: true });
|
|
260
|
+
expect(validateServerUrl("https://secure.example.com/api")).toEqual({
|
|
261
|
+
valid: true,
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
it("should accept localhost URLs (with warning)", () => {
|
|
265
|
+
const result = validateServerUrl("http://localhost:3000");
|
|
266
|
+
expect(result.valid).toBe(true);
|
|
267
|
+
expect(result.error).toBe("PRIVATE_WARNING");
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
describe("Invalid URL protocols", () => {
|
|
271
|
+
it("should reject file URLs", () => {
|
|
272
|
+
expect(validateServerUrl("file:///etc/passwd").valid).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
it("should reject FTP URLs", () => {
|
|
275
|
+
expect(validateServerUrl("ftp://example.com").valid).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
it("should reject custom protocols", () => {
|
|
278
|
+
expect(validateServerUrl("gopher://example.com").valid).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
describe("Private IP detection (SSRF prevention)", () => {
|
|
282
|
+
it("should detect localhost variants", () => {
|
|
283
|
+
expect(isPrivateHostname("localhost")).toBe(true);
|
|
284
|
+
expect(isPrivateHostname("localhost.localdomain")).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
it("should detect IPv4 loopback", () => {
|
|
287
|
+
expect(isPrivateHostname("127.0.0.1")).toBe(true);
|
|
288
|
+
expect(isPrivateHostname("127.1.1.1")).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
it("should detect IPv4 private ranges", () => {
|
|
291
|
+
expect(isPrivateHostname("10.0.0.1")).toBe(true);
|
|
292
|
+
expect(isPrivateHostname("172.16.0.1")).toBe(true);
|
|
293
|
+
expect(isPrivateHostname("192.168.1.1")).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
it("should detect link-local addresses", () => {
|
|
296
|
+
expect(isPrivateHostname("169.254.1.1")).toBe(true);
|
|
297
|
+
});
|
|
298
|
+
it("should detect cloud metadata endpoints", () => {
|
|
299
|
+
expect(isPrivateHostname("169.254.169.254")).toBe(true);
|
|
300
|
+
expect(isPrivateHostname("metadata.google.internal")).toBe(true);
|
|
301
|
+
});
|
|
302
|
+
it("should detect IPv6 private ranges", () => {
|
|
303
|
+
expect(isPrivateHostname("[::1]")).toBe(true);
|
|
304
|
+
expect(isPrivateHostname("[fe80::1]")).toBe(true);
|
|
305
|
+
expect(isPrivateHostname("[fc00::1]")).toBe(true);
|
|
306
|
+
expect(isPrivateHostname("[fd00::1]")).toBe(true);
|
|
307
|
+
});
|
|
308
|
+
it("should allow public addresses", () => {
|
|
309
|
+
expect(isPrivateHostname("example.com")).toBe(false);
|
|
310
|
+
expect(isPrivateHostname("8.8.8.8")).toBe(false);
|
|
311
|
+
expect(isPrivateHostname("1.1.1.1")).toBe(false);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
describe("Malformed URLs", () => {
|
|
315
|
+
it("should reject invalid URL formats", () => {
|
|
316
|
+
expect(validateServerUrl("not-a-url").valid).toBe(false);
|
|
317
|
+
expect(validateServerUrl("://missing-protocol").valid).toBe(false);
|
|
318
|
+
expect(validateServerUrl("").valid).toBe(false);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
describe("Command Validation (Injection Prevention)", () => {
|
|
323
|
+
/**
|
|
324
|
+
* Recreates validateCommand logic from cli.ts
|
|
325
|
+
*/
|
|
326
|
+
function validateCommand(command) {
|
|
327
|
+
const dangerousChars = /[;&|`$(){}[\]<>!]/;
|
|
328
|
+
if (dangerousChars.test(command)) {
|
|
329
|
+
return {
|
|
330
|
+
valid: false,
|
|
331
|
+
error: `Invalid command: contains shell metacharacters: ${command}`,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return { valid: true };
|
|
335
|
+
}
|
|
336
|
+
describe("Valid commands", () => {
|
|
337
|
+
it("should accept simple commands", () => {
|
|
338
|
+
expect(validateCommand("node")).toEqual({ valid: true });
|
|
339
|
+
expect(validateCommand("python3")).toEqual({ valid: true });
|
|
340
|
+
expect(validateCommand("/usr/bin/node")).toEqual({ valid: true });
|
|
341
|
+
});
|
|
342
|
+
it("should accept commands with safe characters", () => {
|
|
343
|
+
expect(validateCommand("my-command")).toEqual({ valid: true });
|
|
344
|
+
expect(validateCommand("command_name")).toEqual({ valid: true });
|
|
345
|
+
expect(validateCommand("command.exe")).toEqual({ valid: true });
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
describe("Commands with shell metacharacters", () => {
|
|
349
|
+
it("should reject commands with semicolons", () => {
|
|
350
|
+
expect(validateCommand("cmd; rm -rf /").valid).toBe(false);
|
|
351
|
+
});
|
|
352
|
+
it("should reject commands with pipes", () => {
|
|
353
|
+
expect(validateCommand("cat file | bash").valid).toBe(false);
|
|
354
|
+
});
|
|
355
|
+
it("should reject commands with ampersands", () => {
|
|
356
|
+
expect(validateCommand("cmd && malicious").valid).toBe(false);
|
|
357
|
+
expect(validateCommand("cmd & background").valid).toBe(false);
|
|
358
|
+
});
|
|
359
|
+
it("should reject commands with backticks", () => {
|
|
360
|
+
expect(validateCommand("cmd `whoami`").valid).toBe(false);
|
|
361
|
+
});
|
|
362
|
+
it("should reject commands with dollar signs", () => {
|
|
363
|
+
expect(validateCommand("cmd $(whoami)").valid).toBe(false);
|
|
364
|
+
expect(validateCommand("cmd $VAR").valid).toBe(false);
|
|
365
|
+
});
|
|
366
|
+
it("should reject commands with redirects", () => {
|
|
367
|
+
expect(validateCommand("cmd > output").valid).toBe(false);
|
|
368
|
+
expect(validateCommand("cmd < input").valid).toBe(false);
|
|
369
|
+
});
|
|
370
|
+
it("should reject commands with brackets", () => {
|
|
371
|
+
expect(validateCommand("cmd [arg]").valid).toBe(false);
|
|
372
|
+
expect(validateCommand("cmd {arg}").valid).toBe(false);
|
|
373
|
+
});
|
|
374
|
+
it("should reject commands with parentheses", () => {
|
|
375
|
+
expect(validateCommand("(cmd)").valid).toBe(false);
|
|
376
|
+
});
|
|
377
|
+
it("should reject commands with exclamation marks", () => {
|
|
378
|
+
expect(validateCommand("cmd !arg").valid).toBe(false);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
describe("Module Name Validation", () => {
|
|
383
|
+
/**
|
|
384
|
+
* Recreates validateModuleNames logic from assess-full.ts
|
|
385
|
+
*/
|
|
386
|
+
const VALID_MODULE_NAMES = [
|
|
387
|
+
"functionality",
|
|
388
|
+
"security",
|
|
389
|
+
"errorHandling",
|
|
390
|
+
"protocolCompliance",
|
|
391
|
+
"aupCompliance",
|
|
392
|
+
"toolAnnotations",
|
|
393
|
+
"prohibitedLibraries",
|
|
394
|
+
"manifestValidation",
|
|
395
|
+
"authentication",
|
|
396
|
+
"temporal",
|
|
397
|
+
"resources",
|
|
398
|
+
"prompts",
|
|
399
|
+
"crossCapability",
|
|
400
|
+
"developerExperience",
|
|
401
|
+
"portability",
|
|
402
|
+
"externalAPIScanner",
|
|
403
|
+
];
|
|
404
|
+
function validateModuleNames(input) {
|
|
405
|
+
const names = input
|
|
406
|
+
.split(",")
|
|
407
|
+
.map((n) => n.trim())
|
|
408
|
+
.filter(Boolean);
|
|
409
|
+
const invalid = names.filter((n) => !VALID_MODULE_NAMES.includes(n));
|
|
410
|
+
if (invalid.length > 0) {
|
|
411
|
+
return { valid: false, invalid };
|
|
412
|
+
}
|
|
413
|
+
return { valid: true, names };
|
|
414
|
+
}
|
|
415
|
+
describe("Valid module names", () => {
|
|
416
|
+
it("should accept single valid module", () => {
|
|
417
|
+
expect(validateModuleNames("security")).toEqual({
|
|
418
|
+
valid: true,
|
|
419
|
+
names: ["security"],
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
it("should accept comma-separated modules", () => {
|
|
423
|
+
expect(validateModuleNames("security,functionality,temporal")).toEqual({
|
|
424
|
+
valid: true,
|
|
425
|
+
names: ["security", "functionality", "temporal"],
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
it("should trim whitespace", () => {
|
|
429
|
+
expect(validateModuleNames(" security , functionality , temporal ")).toEqual({
|
|
430
|
+
valid: true,
|
|
431
|
+
names: ["security", "functionality", "temporal"],
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
it("should filter empty values", () => {
|
|
435
|
+
expect(validateModuleNames("security,,functionality")).toEqual({
|
|
436
|
+
valid: true,
|
|
437
|
+
names: ["security", "functionality"],
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
describe("Invalid module names", () => {
|
|
442
|
+
it("should reject invalid module names", () => {
|
|
443
|
+
const result = validateModuleNames("invalid,security");
|
|
444
|
+
expect(result.valid).toBe(false);
|
|
445
|
+
expect(result.invalid).toEqual(["invalid"]);
|
|
446
|
+
});
|
|
447
|
+
it("should reject all invalid if multiple", () => {
|
|
448
|
+
const result = validateModuleNames("invalid1,invalid2,security");
|
|
449
|
+
expect(result.valid).toBe(false);
|
|
450
|
+
expect(result.invalid).toEqual(["invalid1", "invalid2"]);
|
|
451
|
+
});
|
|
452
|
+
it("should reject case-sensitive mismatches", () => {
|
|
453
|
+
const result = validateModuleNames("SECURITY");
|
|
454
|
+
expect(result.valid).toBe(false);
|
|
455
|
+
expect(result.invalid).toEqual(["SECURITY"]);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
describe("Profile Validation", () => {
|
|
460
|
+
/**
|
|
461
|
+
* Profile validation logic from assess-full.ts
|
|
462
|
+
*/
|
|
463
|
+
const VALID_PROFILES = ["quick", "security", "compliance", "full"];
|
|
464
|
+
function isValidProfileName(name) {
|
|
465
|
+
return VALID_PROFILES.includes(name);
|
|
466
|
+
}
|
|
467
|
+
describe("Valid profiles", () => {
|
|
468
|
+
it("should accept valid profile names", () => {
|
|
469
|
+
expect(isValidProfileName("quick")).toBe(true);
|
|
470
|
+
expect(isValidProfileName("security")).toBe(true);
|
|
471
|
+
expect(isValidProfileName("compliance")).toBe(true);
|
|
472
|
+
expect(isValidProfileName("full")).toBe(true);
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
describe("Invalid profiles", () => {
|
|
476
|
+
it("should reject invalid profile names", () => {
|
|
477
|
+
expect(isValidProfileName("invalid")).toBe(false);
|
|
478
|
+
expect(isValidProfileName("QUICK")).toBe(false);
|
|
479
|
+
expect(isValidProfileName("")).toBe(false);
|
|
480
|
+
expect(isValidProfileName("custom")).toBe(false);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
describe("Format Validation", () => {
|
|
485
|
+
function isValidFormat(format) {
|
|
486
|
+
return format === "json" || format === "markdown";
|
|
487
|
+
}
|
|
488
|
+
describe("Valid formats", () => {
|
|
489
|
+
it("should accept json format", () => {
|
|
490
|
+
expect(isValidFormat("json")).toBe(true);
|
|
491
|
+
});
|
|
492
|
+
it("should accept markdown format", () => {
|
|
493
|
+
expect(isValidFormat("markdown")).toBe(true);
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
describe("Invalid formats", () => {
|
|
497
|
+
it("should reject invalid formats", () => {
|
|
498
|
+
expect(isValidFormat("html")).toBe(false);
|
|
499
|
+
expect(isValidFormat("xml")).toBe(false);
|
|
500
|
+
expect(isValidFormat("JSON")).toBe(false);
|
|
501
|
+
expect(isValidFormat("")).toBe(false);
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
describe("Mutual Exclusivity Validation", () => {
|
|
506
|
+
function validateMutualExclusivity(options) {
|
|
507
|
+
if (options.profile &&
|
|
508
|
+
(options.skipModules?.length || options.onlyModules?.length)) {
|
|
509
|
+
return {
|
|
510
|
+
valid: false,
|
|
511
|
+
error: "--profile cannot be used with --skip-modules or --only-modules",
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
if (options.skipModules?.length && options.onlyModules?.length) {
|
|
515
|
+
return {
|
|
516
|
+
valid: false,
|
|
517
|
+
error: "--skip-modules and --only-modules are mutually exclusive",
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
return { valid: true };
|
|
521
|
+
}
|
|
522
|
+
describe("Valid flag combinations", () => {
|
|
523
|
+
it("should allow profile alone", () => {
|
|
524
|
+
expect(validateMutualExclusivity({ profile: "quick" })).toEqual({
|
|
525
|
+
valid: true,
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
it("should allow skip-modules alone", () => {
|
|
529
|
+
expect(validateMutualExclusivity({ skipModules: ["temporal"] })).toEqual({
|
|
530
|
+
valid: true,
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
it("should allow only-modules alone", () => {
|
|
534
|
+
expect(validateMutualExclusivity({ onlyModules: ["security"] })).toEqual({
|
|
535
|
+
valid: true,
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
it("should allow no module selection flags", () => {
|
|
539
|
+
expect(validateMutualExclusivity({})).toEqual({ valid: true });
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
describe("Invalid flag combinations", () => {
|
|
543
|
+
it("should reject profile + skip-modules", () => {
|
|
544
|
+
const result = validateMutualExclusivity({
|
|
545
|
+
profile: "quick",
|
|
546
|
+
skipModules: ["temporal"],
|
|
547
|
+
});
|
|
548
|
+
expect(result.valid).toBe(false);
|
|
549
|
+
expect(result.error).toContain("--profile cannot be used");
|
|
550
|
+
});
|
|
551
|
+
it("should reject profile + only-modules", () => {
|
|
552
|
+
const result = validateMutualExclusivity({
|
|
553
|
+
profile: "quick",
|
|
554
|
+
onlyModules: ["security"],
|
|
555
|
+
});
|
|
556
|
+
expect(result.valid).toBe(false);
|
|
557
|
+
expect(result.error).toContain("--profile cannot be used");
|
|
558
|
+
});
|
|
559
|
+
it("should reject skip-modules + only-modules", () => {
|
|
560
|
+
const result = validateMutualExclusivity({
|
|
561
|
+
skipModules: ["temporal"],
|
|
562
|
+
onlyModules: ["security"],
|
|
563
|
+
});
|
|
564
|
+
expect(result.valid).toBe(false);
|
|
565
|
+
expect(result.error).toContain("mutually exclusive");
|
|
566
|
+
});
|
|
567
|
+
it("should reject all three flags together", () => {
|
|
568
|
+
const result = validateMutualExclusivity({
|
|
569
|
+
profile: "quick",
|
|
570
|
+
skipModules: ["temporal"],
|
|
571
|
+
onlyModules: ["security"],
|
|
572
|
+
});
|
|
573
|
+
expect(result.valid).toBe(false);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
describe("Log Level Validation", () => {
|
|
578
|
+
function isValidLogLevel(level) {
|
|
579
|
+
const validLevels = [
|
|
580
|
+
"silent",
|
|
581
|
+
"error",
|
|
582
|
+
"warn",
|
|
583
|
+
"info",
|
|
584
|
+
"debug",
|
|
585
|
+
];
|
|
586
|
+
return validLevels.includes(level);
|
|
587
|
+
}
|
|
588
|
+
describe("Valid log levels", () => {
|
|
589
|
+
it("should accept all valid log levels", () => {
|
|
590
|
+
expect(isValidLogLevel("silent")).toBe(true);
|
|
591
|
+
expect(isValidLogLevel("error")).toBe(true);
|
|
592
|
+
expect(isValidLogLevel("warn")).toBe(true);
|
|
593
|
+
expect(isValidLogLevel("info")).toBe(true);
|
|
594
|
+
expect(isValidLogLevel("debug")).toBe(true);
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
describe("Invalid log levels", () => {
|
|
598
|
+
it("should reject invalid log levels", () => {
|
|
599
|
+
expect(isValidLogLevel("verbose")).toBe(false);
|
|
600
|
+
expect(isValidLogLevel("trace")).toBe(false);
|
|
601
|
+
expect(isValidLogLevel("INFO")).toBe(false);
|
|
602
|
+
expect(isValidLogLevel("")).toBe(false);
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
});
|