@avi770/testteam 1.2.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.
- package/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/agents/01-analyst.ts +100 -0
- package/agents/02-seed-architect.ts +59 -0
- package/agents/03-test-generator.ts +191 -0
- package/agents/04-unit-runner.ts +160 -0
- package/agents/05-browser-crawler.ts +790 -0
- package/agents/06-api-exerciser.ts +311 -0
- package/agents/07-security-scout.ts +188 -0
- package/agents/08-a11y-guardian.ts +212 -0
- package/agents/09-healer.ts +228 -0
- package/agents/10-reporter.ts +266 -0
- package/agents/11-fixer.ts +253 -0
- package/agents/12-ux-inspector.ts +444 -0
- package/agents/13-performance-profiler.ts +271 -0
- package/agents/14-data-integrity-auditor.ts +417 -0
- package/agents/15-regression-sentinel.ts +307 -0
- package/agents/16-chaos-agent.ts +228 -0
- package/agents/17-documentation-validator.ts +266 -0
- package/agents/18-integration-watchdog.ts +178 -0
- package/agents/19-tenant-isolation-auditor.ts +199 -0
- package/agents/20-workflow-completion-tester.ts +203 -0
- package/agents/21-state-session-tester.ts +262 -0
- package/agents/22-email-notification-verifier.ts +244 -0
- package/agents/23-migration-tester.ts +80 -0
- package/agents/__tests__/01-analyst.test.ts +188 -0
- package/agents/__tests__/02-seed-architect.test.ts +152 -0
- package/agents/__tests__/03-test-generator-full.test.ts +321 -0
- package/agents/__tests__/03-test-generator.test.ts +318 -0
- package/agents/__tests__/04-unit-runner.test.ts +320 -0
- package/agents/__tests__/05-browser-crawler-beta.test.ts +492 -0
- package/agents/__tests__/05-browser-crawler-release.test.ts +412 -0
- package/agents/__tests__/05-browser-crawler-uat.test.ts +578 -0
- package/agents/__tests__/05-browser-crawler.test.ts +518 -0
- package/agents/__tests__/06-api-exerciser.test.ts +619 -0
- package/agents/__tests__/07-security-scout.test.ts +382 -0
- package/agents/__tests__/08-a11y-guardian.test.ts +530 -0
- package/agents/__tests__/09-healer.test.ts +384 -0
- package/agents/__tests__/10-reporter.test.ts +366 -0
- package/agents/__tests__/11-fixer.test.ts +406 -0
- package/agents/__tests__/12-ux-inspector-extended.test.ts +465 -0
- package/agents/__tests__/12-ux-inspector.test.ts +443 -0
- package/agents/__tests__/13-performance-profiler.test.ts +411 -0
- package/agents/__tests__/14-data-integrity-auditor-extended.test.ts +573 -0
- package/agents/__tests__/14-data-integrity-auditor.test.ts +407 -0
- package/agents/__tests__/15-regression-sentinel.test.ts +657 -0
- package/agents/__tests__/16-chaos-agent.test.ts +427 -0
- package/agents/__tests__/17-documentation-validator.test.ts +402 -0
- package/agents/__tests__/18-integration-watchdog.test.ts +263 -0
- package/agents/__tests__/19-tenant-isolation-auditor.test.ts +400 -0
- package/agents/__tests__/20-workflow-completion-tester.test.ts +586 -0
- package/agents/__tests__/21-state-session-tester.test.ts +374 -0
- package/agents/__tests__/22-email-notification-verifier.test.ts +441 -0
- package/agents/__tests__/23-migration-tester.test.ts +145 -0
- package/agents/__tests__/base-agent.test.ts +188 -0
- package/agents/__tests__/registry.test.ts +218 -0
- package/agents/base-agent.ts +77 -0
- package/agents/registry.ts +136 -0
- package/baselines/api-schemas/.gitkeep +0 -0
- package/baselines/performance/.gitkeep +0 -0
- package/baselines/screenshots/.gitkeep +0 -0
- package/bin/testteam.js +10 -0
- package/core/__tests__/ci-output.test.ts +430 -0
- package/core/__tests__/cli.test.ts +387 -0
- package/core/__tests__/config.test.ts +78 -0
- package/core/__tests__/cost-tracker.test.ts +158 -0
- package/core/__tests__/evidence.test.ts +265 -0
- package/core/__tests__/fix-loop.test.ts +210 -0
- package/core/__tests__/health-check.test.ts +44 -0
- package/core/__tests__/init.test.ts +609 -0
- package/core/__tests__/integration.test.ts +204 -0
- package/core/__tests__/license-gen.test.ts +227 -0
- package/core/__tests__/license.test.ts +326 -0
- package/core/__tests__/multi-browser.test.ts +278 -0
- package/core/__tests__/orchestrator.test.ts +519 -0
- package/core/__tests__/phase-gate.test.ts +43 -0
- package/core/__tests__/report-html.test.ts +398 -0
- package/core/__tests__/report-upload.test.ts +325 -0
- package/core/__tests__/run-counter.test.ts +234 -0
- package/core/ci-output.ts +240 -0
- package/core/cli.ts +232 -0
- package/core/config.ts +178 -0
- package/core/cost-tracker.ts +59 -0
- package/core/evidence.ts +132 -0
- package/core/fix-loop.ts +85 -0
- package/core/health-check.ts +54 -0
- package/core/init.ts +546 -0
- package/core/license-gen.ts +212 -0
- package/core/license.ts +211 -0
- package/core/messages.ts +67 -0
- package/core/multi-browser.ts +136 -0
- package/core/orchestrator.ts +354 -0
- package/core/phase-gate.ts +55 -0
- package/core/report-html.ts +657 -0
- package/core/report-upload.ts +188 -0
- package/core/run-counter.ts +175 -0
- package/core/types.ts +57 -0
- package/dist/agents/01-analyst.d.ts +11 -0
- package/dist/agents/01-analyst.d.ts.map +1 -0
- package/dist/agents/01-analyst.js +75 -0
- package/dist/agents/01-analyst.js.map +1 -0
- package/dist/agents/02-seed-architect.d.ts +11 -0
- package/dist/agents/02-seed-architect.d.ts.map +1 -0
- package/dist/agents/02-seed-architect.js +51 -0
- package/dist/agents/02-seed-architect.js.map +1 -0
- package/dist/agents/03-test-generator.d.ts +9 -0
- package/dist/agents/03-test-generator.d.ts.map +1 -0
- package/dist/agents/03-test-generator.js +167 -0
- package/dist/agents/03-test-generator.js.map +1 -0
- package/dist/agents/04-unit-runner.d.ts +9 -0
- package/dist/agents/04-unit-runner.d.ts.map +1 -0
- package/dist/agents/04-unit-runner.js +113 -0
- package/dist/agents/04-unit-runner.js.map +1 -0
- package/dist/agents/05-browser-crawler.d.ts +30 -0
- package/dist/agents/05-browser-crawler.d.ts.map +1 -0
- package/dist/agents/05-browser-crawler.js +685 -0
- package/dist/agents/05-browser-crawler.js.map +1 -0
- package/dist/agents/06-api-exerciser.d.ts +23 -0
- package/dist/agents/06-api-exerciser.d.ts.map +1 -0
- package/dist/agents/06-api-exerciser.js +253 -0
- package/dist/agents/06-api-exerciser.js.map +1 -0
- package/dist/agents/07-security-scout.d.ts +11 -0
- package/dist/agents/07-security-scout.d.ts.map +1 -0
- package/dist/agents/07-security-scout.js +142 -0
- package/dist/agents/07-security-scout.js.map +1 -0
- package/dist/agents/08-a11y-guardian.d.ts +13 -0
- package/dist/agents/08-a11y-guardian.d.ts.map +1 -0
- package/dist/agents/08-a11y-guardian.js +176 -0
- package/dist/agents/08-a11y-guardian.js.map +1 -0
- package/dist/agents/09-healer.d.ts +33 -0
- package/dist/agents/09-healer.d.ts.map +1 -0
- package/dist/agents/09-healer.js +167 -0
- package/dist/agents/09-healer.js.map +1 -0
- package/dist/agents/10-reporter.d.ts +26 -0
- package/dist/agents/10-reporter.d.ts.map +1 -0
- package/dist/agents/10-reporter.js +215 -0
- package/dist/agents/10-reporter.js.map +1 -0
- package/dist/agents/11-fixer.d.ts +26 -0
- package/dist/agents/11-fixer.d.ts.map +1 -0
- package/dist/agents/11-fixer.js +195 -0
- package/dist/agents/11-fixer.js.map +1 -0
- package/dist/agents/12-ux-inspector.d.ts +15 -0
- package/dist/agents/12-ux-inspector.d.ts.map +1 -0
- package/dist/agents/12-ux-inspector.js +364 -0
- package/dist/agents/12-ux-inspector.js.map +1 -0
- package/dist/agents/13-performance-profiler.d.ts +13 -0
- package/dist/agents/13-performance-profiler.d.ts.map +1 -0
- package/dist/agents/13-performance-profiler.js +216 -0
- package/dist/agents/13-performance-profiler.js.map +1 -0
- package/dist/agents/14-data-integrity-auditor.d.ts +12 -0
- package/dist/agents/14-data-integrity-auditor.d.ts.map +1 -0
- package/dist/agents/14-data-integrity-auditor.js +356 -0
- package/dist/agents/14-data-integrity-auditor.js.map +1 -0
- package/dist/agents/15-regression-sentinel.d.ts +25 -0
- package/dist/agents/15-regression-sentinel.d.ts.map +1 -0
- package/dist/agents/15-regression-sentinel.js +251 -0
- package/dist/agents/15-regression-sentinel.js.map +1 -0
- package/dist/agents/16-chaos-agent.d.ts +9 -0
- package/dist/agents/16-chaos-agent.d.ts.map +1 -0
- package/dist/agents/16-chaos-agent.js +207 -0
- package/dist/agents/16-chaos-agent.js.map +1 -0
- package/dist/agents/17-documentation-validator.d.ts +31 -0
- package/dist/agents/17-documentation-validator.d.ts.map +1 -0
- package/dist/agents/17-documentation-validator.js +246 -0
- package/dist/agents/17-documentation-validator.js.map +1 -0
- package/dist/agents/18-integration-watchdog.d.ts +10 -0
- package/dist/agents/18-integration-watchdog.d.ts.map +1 -0
- package/dist/agents/18-integration-watchdog.js +138 -0
- package/dist/agents/18-integration-watchdog.js.map +1 -0
- package/dist/agents/19-tenant-isolation-auditor.d.ts +9 -0
- package/dist/agents/19-tenant-isolation-auditor.d.ts.map +1 -0
- package/dist/agents/19-tenant-isolation-auditor.js +166 -0
- package/dist/agents/19-tenant-isolation-auditor.js.map +1 -0
- package/dist/agents/20-workflow-completion-tester.d.ts +12 -0
- package/dist/agents/20-workflow-completion-tester.d.ts.map +1 -0
- package/dist/agents/20-workflow-completion-tester.js +159 -0
- package/dist/agents/20-workflow-completion-tester.js.map +1 -0
- package/dist/agents/21-state-session-tester.d.ts +10 -0
- package/dist/agents/21-state-session-tester.d.ts.map +1 -0
- package/dist/agents/21-state-session-tester.js +233 -0
- package/dist/agents/21-state-session-tester.js.map +1 -0
- package/dist/agents/22-email-notification-verifier.d.ts +11 -0
- package/dist/agents/22-email-notification-verifier.d.ts.map +1 -0
- package/dist/agents/22-email-notification-verifier.js +199 -0
- package/dist/agents/22-email-notification-verifier.js.map +1 -0
- package/dist/agents/23-migration-tester.d.ts +10 -0
- package/dist/agents/23-migration-tester.d.ts.map +1 -0
- package/dist/agents/23-migration-tester.js +74 -0
- package/dist/agents/23-migration-tester.js.map +1 -0
- package/dist/agents/base-agent.d.ts +19 -0
- package/dist/agents/base-agent.d.ts.map +1 -0
- package/dist/agents/base-agent.js +67 -0
- package/dist/agents/base-agent.js.map +1 -0
- package/dist/agents/registry.d.ts +29 -0
- package/dist/agents/registry.d.ts.map +1 -0
- package/dist/agents/registry.js +117 -0
- package/dist/agents/registry.js.map +1 -0
- package/dist/core/ci-output.d.ts +35 -0
- package/dist/core/ci-output.d.ts.map +1 -0
- package/dist/core/ci-output.js +193 -0
- package/dist/core/ci-output.js.map +1 -0
- package/dist/core/cli.d.ts +11 -0
- package/dist/core/cli.d.ts.map +1 -0
- package/dist/core/cli.js +197 -0
- package/dist/core/cli.js.map +1 -0
- package/dist/core/config.d.ts +111 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +42 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/cost-tracker.d.ts +22 -0
- package/dist/core/cost-tracker.d.ts.map +1 -0
- package/dist/core/cost-tracker.js +41 -0
- package/dist/core/cost-tracker.js.map +1 -0
- package/dist/core/evidence.d.ts +28 -0
- package/dist/core/evidence.d.ts.map +1 -0
- package/dist/core/evidence.js +95 -0
- package/dist/core/evidence.js.map +1 -0
- package/dist/core/fix-loop.d.ts +29 -0
- package/dist/core/fix-loop.d.ts.map +1 -0
- package/dist/core/fix-loop.js +70 -0
- package/dist/core/fix-loop.js.map +1 -0
- package/dist/core/health-check.d.ts +21 -0
- package/dist/core/health-check.d.ts.map +1 -0
- package/dist/core/health-check.js +26 -0
- package/dist/core/health-check.js.map +1 -0
- package/dist/core/init.d.ts +2 -0
- package/dist/core/init.d.ts.map +1 -0
- package/dist/core/init.js +435 -0
- package/dist/core/init.js.map +1 -0
- package/dist/core/license-gen.d.ts +12 -0
- package/dist/core/license-gen.d.ts.map +1 -0
- package/dist/core/license-gen.js +169 -0
- package/dist/core/license-gen.js.map +1 -0
- package/dist/core/license.d.ts +33 -0
- package/dist/core/license.d.ts.map +1 -0
- package/dist/core/license.js +170 -0
- package/dist/core/license.js.map +1 -0
- package/dist/core/messages.d.ts +10 -0
- package/dist/core/messages.d.ts.map +1 -0
- package/dist/core/messages.js +47 -0
- package/dist/core/messages.js.map +1 -0
- package/dist/core/multi-browser.d.ts +36 -0
- package/dist/core/multi-browser.d.ts.map +1 -0
- package/dist/core/multi-browser.js +88 -0
- package/dist/core/multi-browser.js.map +1 -0
- package/dist/core/orchestrator.d.ts +48 -0
- package/dist/core/orchestrator.d.ts.map +1 -0
- package/dist/core/orchestrator.js +291 -0
- package/dist/core/orchestrator.js.map +1 -0
- package/dist/core/phase-gate.d.ts +4 -0
- package/dist/core/phase-gate.d.ts.map +1 -0
- package/dist/core/phase-gate.js +39 -0
- package/dist/core/phase-gate.js.map +1 -0
- package/dist/core/report-html.d.ts +9 -0
- package/dist/core/report-html.d.ts.map +1 -0
- package/dist/core/report-html.js +617 -0
- package/dist/core/report-html.js.map +1 -0
- package/dist/core/report-upload.d.ts +16 -0
- package/dist/core/report-upload.d.ts.map +1 -0
- package/dist/core/report-upload.js +124 -0
- package/dist/core/report-upload.js.map +1 -0
- package/dist/core/run-counter.d.ts +40 -0
- package/dist/core/run-counter.d.ts.map +1 -0
- package/dist/core/run-counter.js +120 -0
- package/dist/core/run-counter.js.map +1 -0
- package/dist/core/types.d.ts +53 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/helpers/api-client.d.ts +30 -0
- package/dist/helpers/api-client.d.ts.map +1 -0
- package/dist/helpers/api-client.js +77 -0
- package/dist/helpers/api-client.js.map +1 -0
- package/dist/helpers/element-discovery.d.ts +18 -0
- package/dist/helpers/element-discovery.d.ts.map +1 -0
- package/dist/helpers/element-discovery.js +82 -0
- package/dist/helpers/element-discovery.js.map +1 -0
- package/dist/helpers/env-resolver.d.ts +29 -0
- package/dist/helpers/env-resolver.d.ts.map +1 -0
- package/dist/helpers/env-resolver.js +51 -0
- package/dist/helpers/env-resolver.js.map +1 -0
- package/dist/helpers/form-filler.d.ts +13 -0
- package/dist/helpers/form-filler.d.ts.map +1 -0
- package/dist/helpers/form-filler.js +98 -0
- package/dist/helpers/form-filler.js.map +1 -0
- package/dist/helpers/modal-handler.d.ts +16 -0
- package/dist/helpers/modal-handler.d.ts.map +1 -0
- package/dist/helpers/modal-handler.js +95 -0
- package/dist/helpers/modal-handler.js.map +1 -0
- package/dist/helpers/navigation.d.ts +37 -0
- package/dist/helpers/navigation.d.ts.map +1 -0
- package/dist/helpers/navigation.js +83 -0
- package/dist/helpers/navigation.js.map +1 -0
- package/dist/helpers/quality-gate.d.ts +17 -0
- package/dist/helpers/quality-gate.d.ts.map +1 -0
- package/dist/helpers/quality-gate.js +144 -0
- package/dist/helpers/quality-gate.js.map +1 -0
- package/dist/helpers/screenshot.d.ts +24 -0
- package/dist/helpers/screenshot.d.ts.map +1 -0
- package/dist/helpers/screenshot.js +76 -0
- package/dist/helpers/screenshot.js.map +1 -0
- package/dist/helpers/seed-validator.d.ts +15 -0
- package/dist/helpers/seed-validator.d.ts.map +1 -0
- package/dist/helpers/seed-validator.js +53 -0
- package/dist/helpers/seed-validator.js.map +1 -0
- package/helpers/__tests__/api-client.test.ts +199 -0
- package/helpers/__tests__/element-discovery.test.ts +202 -0
- package/helpers/__tests__/form-filler-extended.test.ts +212 -0
- package/helpers/__tests__/form-filler.test.ts +99 -0
- package/helpers/__tests__/modal-handler.test.ts +152 -0
- package/helpers/__tests__/navigation.test.ts +214 -0
- package/helpers/__tests__/quality-gate.test.ts +117 -0
- package/helpers/__tests__/screenshot.test.ts +139 -0
- package/helpers/__tests__/seed-validator.test.ts +114 -0
- package/helpers/api-client.ts +111 -0
- package/helpers/element-discovery.ts +105 -0
- package/helpers/env-resolver.ts +69 -0
- package/helpers/form-filler.ts +126 -0
- package/helpers/modal-handler.ts +108 -0
- package/helpers/navigation.ts +100 -0
- package/helpers/quality-gate.ts +180 -0
- package/helpers/screenshot.ts +111 -0
- package/helpers/seed-validator.ts +70 -0
- package/package.json +88 -0
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import type { ValidatedConfig } from '../../core/config';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Stub config
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
const stubConfig: ValidatedConfig = {
|
|
9
|
+
schemaVersion: 1,
|
|
10
|
+
name: 'test',
|
|
11
|
+
auth: {
|
|
12
|
+
loginUrl: 'http://localhost/login',
|
|
13
|
+
credentials: { admin: { email: 'admin@test.com', password: 'secret' } },
|
|
14
|
+
},
|
|
15
|
+
modules: [{ id: 'dashboard', route: '/dashboard', sidebarIcon: true }],
|
|
16
|
+
environments: {
|
|
17
|
+
alpha: { baseUrl: 'http://localhost:3000', seed: false },
|
|
18
|
+
},
|
|
19
|
+
portals: [],
|
|
20
|
+
breakpoints: [],
|
|
21
|
+
integrations: [],
|
|
22
|
+
workers: [],
|
|
23
|
+
workflows: [],
|
|
24
|
+
accuracy: { enabled: false, decimalPrecision: 2, currencySymbol: '$', timezone: 'UTC' },
|
|
25
|
+
tenancy: { enabled: false, isolationField: 'firmId', testFirms: [] },
|
|
26
|
+
costBudget: { maxPerRun: 100, maxPerAgent: 10, alertThreshold: 0.8, onExceeded: 'abort' },
|
|
27
|
+
agentClassification: { blocking: [], advisory: [] },
|
|
28
|
+
thresholds: {
|
|
29
|
+
unitCoverage: { lines: 80, functions: 80, branches: 80, statements: 80 },
|
|
30
|
+
lighthouse: { accessibility: 90, performance: 90, bestPractices: 90, seo: 90 },
|
|
31
|
+
apiResponseTime: 2000,
|
|
32
|
+
bundleSizeLimit: 500000,
|
|
33
|
+
maxConsoleErrors: 0,
|
|
34
|
+
},
|
|
35
|
+
disabledAgents: [],
|
|
36
|
+
disabledAgentSet: new Set<number>(),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Mock node:fs/promises (readdir + readFile)
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
const readdirMock = vi.fn();
|
|
44
|
+
const readFileMock = vi.fn();
|
|
45
|
+
|
|
46
|
+
vi.mock('node:fs/promises', () => ({
|
|
47
|
+
readdir: (...args: unknown[]) => readdirMock(...args),
|
|
48
|
+
readFile: (...args: unknown[]) => readFileMock(...args),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Mock node:path (so join returns predictable values in tests)
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
vi.mock('node:path', async (importOriginal) => {
|
|
56
|
+
const original = await importOriginal<typeof import('node:path')>();
|
|
57
|
+
return { ...original };
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Mock ApiClient
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
const getMock = vi.fn();
|
|
65
|
+
const postMock = vi.fn();
|
|
66
|
+
const putMock = vi.fn();
|
|
67
|
+
const deleteMock = vi.fn();
|
|
68
|
+
const createAuthenticatedMock = vi.fn();
|
|
69
|
+
|
|
70
|
+
vi.mock('../../helpers/api-client', () => ({
|
|
71
|
+
ApiClient: class MockApiClient {
|
|
72
|
+
static createAuthenticated = (...args: unknown[]) => createAuthenticatedMock(...args);
|
|
73
|
+
get = (...args: unknown[]) => getMock(...args);
|
|
74
|
+
post = (...args: unknown[]) => postMock(...args);
|
|
75
|
+
put = (...args: unknown[]) => putMock(...args);
|
|
76
|
+
delete = (...args: unknown[]) => deleteMock(...args);
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Mock fetch (preFlight URL check)
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
const fetchMock = vi.fn();
|
|
85
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Dynamic import AFTER mocks
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
const { ApiExerciserAgent } = await import('../06-api-exerciser');
|
|
92
|
+
|
|
93
|
+
function createAgent(): InstanceType<typeof ApiExerciserAgent> {
|
|
94
|
+
return new ApiExerciserAgent(stubConfig, 'beta', '/tmp/test-run');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function makeOkResponse(status = 200, durationMs = 50): object {
|
|
98
|
+
return { status, body: {}, headers: {}, durationMs };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Tests
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
describe('ApiExerciserAgent', () => {
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
readdirMock.mockReset();
|
|
108
|
+
readFileMock.mockReset();
|
|
109
|
+
getMock.mockReset();
|
|
110
|
+
postMock.mockReset();
|
|
111
|
+
putMock.mockReset();
|
|
112
|
+
deleteMock.mockReset();
|
|
113
|
+
createAuthenticatedMock.mockReset();
|
|
114
|
+
fetchMock.mockReset();
|
|
115
|
+
|
|
116
|
+
// Default: preFlight succeeds
|
|
117
|
+
fetchMock.mockResolvedValue({ ok: true, status: 200 });
|
|
118
|
+
|
|
119
|
+
// Default: no route files
|
|
120
|
+
readdirMock.mockResolvedValue([]);
|
|
121
|
+
|
|
122
|
+
// Default: authenticated client creation succeeds
|
|
123
|
+
createAuthenticatedMock.mockResolvedValue({
|
|
124
|
+
get: getMock,
|
|
125
|
+
post: postMock,
|
|
126
|
+
put: putMock,
|
|
127
|
+
delete: deleteMock,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Default: all API calls return 200 quickly
|
|
131
|
+
getMock.mockResolvedValue(makeOkResponse(200));
|
|
132
|
+
postMock.mockResolvedValue(makeOkResponse(200));
|
|
133
|
+
putMock.mockResolvedValue(makeOkResponse(200));
|
|
134
|
+
deleteMock.mockResolvedValue(makeOkResponse(200));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// -------------------------------------------------------------------------
|
|
138
|
+
// Identity
|
|
139
|
+
// -------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
it('has agentId 6 and agentName "API Exerciser"', () => {
|
|
142
|
+
const agent = createAgent();
|
|
143
|
+
expect(agent.agentId).toBe(6);
|
|
144
|
+
expect(agent.agentName).toBe('API Exerciser');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// -------------------------------------------------------------------------
|
|
148
|
+
// preFlight
|
|
149
|
+
// -------------------------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
describe('preFlight()', () => {
|
|
152
|
+
it('fails when base URL is not reachable', async () => {
|
|
153
|
+
fetchMock.mockRejectedValue(new Error('ECONNREFUSED'));
|
|
154
|
+
|
|
155
|
+
const agent = createAgent();
|
|
156
|
+
const result = await agent.run();
|
|
157
|
+
|
|
158
|
+
expect(result.status).toBe('failed');
|
|
159
|
+
expect(result.findings[0].description).toMatch(/not reachable/i);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('passes when base URL returns 200', async () => {
|
|
163
|
+
const agent = createAgent();
|
|
164
|
+
const result = await agent.run();
|
|
165
|
+
|
|
166
|
+
expect(result.findings.some(f => f.description.match(/not reachable/i))).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// -------------------------------------------------------------------------
|
|
171
|
+
// Endpoint discovery
|
|
172
|
+
// -------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
describe('endpoint discovery', () => {
|
|
175
|
+
it('discovers GET routes from router.get() patterns in route files', () => {
|
|
176
|
+
const agent = createAgent();
|
|
177
|
+
const content = `
|
|
178
|
+
router.get('/clients', authenticate, getClients);
|
|
179
|
+
router.get('/clients/:id', authenticate, getClient);
|
|
180
|
+
`;
|
|
181
|
+
const endpoints = agent.parseRouteFile(content, 'clients.routes.ts');
|
|
182
|
+
|
|
183
|
+
expect(endpoints.filter(e => e.method === 'GET')).toHaveLength(2);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('discovers POST routes from router.post() patterns', () => {
|
|
187
|
+
const agent = createAgent();
|
|
188
|
+
const content = `router.post('/clients', authenticate, createClient);`;
|
|
189
|
+
const endpoints = agent.parseRouteFile(content, 'clients.routes.ts');
|
|
190
|
+
|
|
191
|
+
expect(endpoints.find(e => e.method === 'POST' && e.path === '/clients')).toBeDefined();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('discovers PUT routes from router.put() patterns', () => {
|
|
195
|
+
const agent = createAgent();
|
|
196
|
+
const content = `router.put('/clients/:id', authenticate, updateClient);`;
|
|
197
|
+
const endpoints = agent.parseRouteFile(content, 'clients.routes.ts');
|
|
198
|
+
|
|
199
|
+
expect(endpoints.find(e => e.method === 'PUT')).toBeDefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('discovers DELETE routes from router.delete() patterns', () => {
|
|
203
|
+
const agent = createAgent();
|
|
204
|
+
const content = `router.delete('/clients/:id', authenticate, deleteClient);`;
|
|
205
|
+
const endpoints = agent.parseRouteFile(content, 'clients.routes.ts');
|
|
206
|
+
|
|
207
|
+
expect(endpoints.find(e => e.method === 'DELETE')).toBeDefined();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('discovers NestJS @Get decorator patterns', () => {
|
|
211
|
+
const agent = createAgent();
|
|
212
|
+
const content = `@Get('/invoices') findAll() {}`;
|
|
213
|
+
const endpoints = agent.parseRouteFile(content, 'invoices.controller.ts');
|
|
214
|
+
|
|
215
|
+
expect(endpoints.find(e => e.method === 'GET' && e.path === '/invoices')).toBeDefined();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('discovers NestJS @Post decorator patterns', () => {
|
|
219
|
+
const agent = createAgent();
|
|
220
|
+
const content = `@Post('/invoices') create() {}`;
|
|
221
|
+
const endpoints = agent.parseRouteFile(content, 'invoices.controller.ts');
|
|
222
|
+
|
|
223
|
+
expect(endpoints.find(e => e.method === 'POST' && e.path === '/invoices')).toBeDefined();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('skips .test.ts files when scanning the routes directory', async () => {
|
|
227
|
+
readdirMock.mockResolvedValue(['clients.routes.ts', 'clients.routes.test.ts']);
|
|
228
|
+
readFileMock.mockResolvedValue(`router.get('/clients', h);`);
|
|
229
|
+
|
|
230
|
+
const agent = createAgent();
|
|
231
|
+
await agent.run();
|
|
232
|
+
|
|
233
|
+
// readFile should only be called once (for the non-test file)
|
|
234
|
+
expect(readFileMock).toHaveBeenCalledTimes(1);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('returns empty findings when routes directory does not exist', async () => {
|
|
238
|
+
readdirMock.mockRejectedValue(new Error('ENOENT: no such file or directory'));
|
|
239
|
+
|
|
240
|
+
const agent = createAgent();
|
|
241
|
+
const result = await agent.run();
|
|
242
|
+
|
|
243
|
+
expect(result.status).toBe('passed');
|
|
244
|
+
expect(result.findings).toHaveLength(0);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// -------------------------------------------------------------------------
|
|
249
|
+
// Auth check
|
|
250
|
+
// -------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
describe('auth check', () => {
|
|
253
|
+
it('creates a security finding when endpoint does not return 401 for anonymous request', async () => {
|
|
254
|
+
readdirMock.mockResolvedValue(['clients.routes.ts']);
|
|
255
|
+
readFileMock.mockResolvedValue(`router.get('/clients', authenticate, getClients);`);
|
|
256
|
+
|
|
257
|
+
// Authenticated client returns 200; anonymous client also returns 200 (missing auth guard)
|
|
258
|
+
const authedInstance = {
|
|
259
|
+
get: vi.fn().mockResolvedValue(makeOkResponse(200)),
|
|
260
|
+
post: postMock,
|
|
261
|
+
put: putMock,
|
|
262
|
+
delete: deleteMock,
|
|
263
|
+
};
|
|
264
|
+
createAuthenticatedMock.mockResolvedValue(authedInstance);
|
|
265
|
+
getMock.mockResolvedValue(makeOkResponse(200)); // anon also gets 200
|
|
266
|
+
|
|
267
|
+
const agent = createAgent();
|
|
268
|
+
const result = await agent.run();
|
|
269
|
+
|
|
270
|
+
const authFinding = result.findings.find(f =>
|
|
271
|
+
f.type === 'code-bug-security' &&
|
|
272
|
+
f.description.includes('did not return 401'),
|
|
273
|
+
);
|
|
274
|
+
expect(authFinding).toBeDefined();
|
|
275
|
+
expect(authFinding!.severity).toBe('high');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('does not create an auth finding when endpoint returns 401 for anonymous request', async () => {
|
|
279
|
+
readdirMock.mockResolvedValue(['clients.routes.ts']);
|
|
280
|
+
readFileMock.mockResolvedValue(`router.get('/clients', authenticate, getClients);`);
|
|
281
|
+
|
|
282
|
+
const authedInstance = {
|
|
283
|
+
get: vi.fn().mockResolvedValue(makeOkResponse(200)),
|
|
284
|
+
post: postMock,
|
|
285
|
+
put: putMock,
|
|
286
|
+
delete: deleteMock,
|
|
287
|
+
};
|
|
288
|
+
createAuthenticatedMock.mockResolvedValue(authedInstance);
|
|
289
|
+
getMock.mockResolvedValue(makeOkResponse(401)); // anon gets 401
|
|
290
|
+
|
|
291
|
+
const agent = createAgent();
|
|
292
|
+
const result = await agent.run();
|
|
293
|
+
|
|
294
|
+
const authFindings = result.findings.filter(f => f.type === 'code-bug-security');
|
|
295
|
+
expect(authFindings).toHaveLength(0);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('skips auth check for /auth routes', async () => {
|
|
299
|
+
readdirMock.mockResolvedValue(['auth.routes.ts']);
|
|
300
|
+
readFileMock.mockResolvedValue(`router.post('/auth/login', loginHandler);`);
|
|
301
|
+
|
|
302
|
+
const authedInstance = {
|
|
303
|
+
get: getMock,
|
|
304
|
+
post: vi.fn().mockResolvedValue(makeOkResponse(200)),
|
|
305
|
+
put: putMock,
|
|
306
|
+
delete: deleteMock,
|
|
307
|
+
};
|
|
308
|
+
createAuthenticatedMock.mockResolvedValue(authedInstance);
|
|
309
|
+
postMock.mockResolvedValue(makeOkResponse(200)); // anon also gets 200, but it's an auth route
|
|
310
|
+
|
|
311
|
+
const agent = createAgent();
|
|
312
|
+
const result = await agent.run();
|
|
313
|
+
|
|
314
|
+
const authFindings = result.findings.filter(f => f.type === 'code-bug-security');
|
|
315
|
+
expect(authFindings).toHaveLength(0);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// -------------------------------------------------------------------------
|
|
320
|
+
// Response time
|
|
321
|
+
// -------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
describe('response time', () => {
|
|
324
|
+
it('creates a medium finding when response time exceeds 500ms', async () => {
|
|
325
|
+
readdirMock.mockResolvedValue(['clients.routes.ts']);
|
|
326
|
+
readFileMock.mockResolvedValue(`router.get('/clients', authenticate, getClients);`);
|
|
327
|
+
|
|
328
|
+
const authedInstance = {
|
|
329
|
+
get: vi.fn().mockResolvedValue(makeOkResponse(200, 600)), // 600ms > 500ms threshold
|
|
330
|
+
post: postMock,
|
|
331
|
+
put: putMock,
|
|
332
|
+
delete: deleteMock,
|
|
333
|
+
};
|
|
334
|
+
createAuthenticatedMock.mockResolvedValue(authedInstance);
|
|
335
|
+
getMock.mockResolvedValue(makeOkResponse(401));
|
|
336
|
+
|
|
337
|
+
const agent = createAgent();
|
|
338
|
+
const result = await agent.run();
|
|
339
|
+
|
|
340
|
+
const slowFinding = result.findings.find(f =>
|
|
341
|
+
f.description.includes('600ms'),
|
|
342
|
+
);
|
|
343
|
+
expect(slowFinding).toBeDefined();
|
|
344
|
+
expect(slowFinding!.severity).toBe('medium');
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// -------------------------------------------------------------------------
|
|
349
|
+
// Validation check
|
|
350
|
+
// -------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
describe('validation check', () => {
|
|
353
|
+
it('creates a finding when POST with empty body returns 200 instead of 400', async () => {
|
|
354
|
+
readdirMock.mockResolvedValue(['clients.routes.ts']);
|
|
355
|
+
readFileMock.mockResolvedValue(`router.post('/clients', authenticate, createClient);`);
|
|
356
|
+
|
|
357
|
+
const authedInstance = {
|
|
358
|
+
get: getMock,
|
|
359
|
+
post: vi.fn().mockResolvedValue(makeOkResponse(200)),
|
|
360
|
+
put: putMock,
|
|
361
|
+
delete: deleteMock,
|
|
362
|
+
};
|
|
363
|
+
createAuthenticatedMock.mockResolvedValue(authedInstance);
|
|
364
|
+
// Anon POST also returns 200 (no validation)
|
|
365
|
+
postMock.mockResolvedValue(makeOkResponse(200));
|
|
366
|
+
|
|
367
|
+
const agent = createAgent();
|
|
368
|
+
const result = await agent.run();
|
|
369
|
+
|
|
370
|
+
const validationFinding = result.findings.find(f =>
|
|
371
|
+
f.description.includes('empty body'),
|
|
372
|
+
);
|
|
373
|
+
expect(validationFinding).toBeDefined();
|
|
374
|
+
expect(validationFinding!.severity).toBe('medium');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('does not create a validation finding when POST with empty body returns 400', async () => {
|
|
378
|
+
readdirMock.mockResolvedValue(['clients.routes.ts']);
|
|
379
|
+
readFileMock.mockResolvedValue(`router.post('/clients', authenticate, createClient);`);
|
|
380
|
+
|
|
381
|
+
const authedInstance = {
|
|
382
|
+
get: getMock,
|
|
383
|
+
post: vi.fn().mockResolvedValue(makeOkResponse(200)),
|
|
384
|
+
put: putMock,
|
|
385
|
+
delete: deleteMock,
|
|
386
|
+
};
|
|
387
|
+
createAuthenticatedMock.mockResolvedValue(authedInstance);
|
|
388
|
+
postMock.mockResolvedValue(makeOkResponse(400));
|
|
389
|
+
|
|
390
|
+
const agent = createAgent();
|
|
391
|
+
const result = await agent.run();
|
|
392
|
+
|
|
393
|
+
const validationFindings = result.findings.filter(f =>
|
|
394
|
+
f.description.includes('empty body'),
|
|
395
|
+
);
|
|
396
|
+
expect(validationFindings).toHaveLength(0);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('does not create a validation finding when POST returns 401 (auth guard first)', async () => {
|
|
400
|
+
readdirMock.mockResolvedValue(['clients.routes.ts']);
|
|
401
|
+
readFileMock.mockResolvedValue(`router.post('/clients', authenticate, createClient);`);
|
|
402
|
+
|
|
403
|
+
const authedInstance = {
|
|
404
|
+
get: getMock,
|
|
405
|
+
post: vi.fn().mockResolvedValue(makeOkResponse(200)),
|
|
406
|
+
put: putMock,
|
|
407
|
+
delete: deleteMock,
|
|
408
|
+
};
|
|
409
|
+
createAuthenticatedMock.mockResolvedValue(authedInstance);
|
|
410
|
+
postMock.mockResolvedValue(makeOkResponse(401));
|
|
411
|
+
|
|
412
|
+
const agent = createAgent();
|
|
413
|
+
const result = await agent.run();
|
|
414
|
+
|
|
415
|
+
const validationFindings = result.findings.filter(f =>
|
|
416
|
+
f.description.includes('empty body'),
|
|
417
|
+
);
|
|
418
|
+
expect(validationFindings).toHaveLength(0);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// -------------------------------------------------------------------------
|
|
423
|
+
// Happy path: error responses
|
|
424
|
+
// -------------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
describe('happy path', () => {
|
|
427
|
+
it('creates a finding when authenticated GET returns 500', async () => {
|
|
428
|
+
readdirMock.mockResolvedValue(['clients.routes.ts']);
|
|
429
|
+
readFileMock.mockResolvedValue(`router.get('/clients', authenticate, getClients);`);
|
|
430
|
+
|
|
431
|
+
const authedInstance = {
|
|
432
|
+
get: vi.fn().mockResolvedValue(makeOkResponse(500)),
|
|
433
|
+
post: postMock,
|
|
434
|
+
put: putMock,
|
|
435
|
+
delete: deleteMock,
|
|
436
|
+
};
|
|
437
|
+
createAuthenticatedMock.mockResolvedValue(authedInstance);
|
|
438
|
+
getMock.mockResolvedValue(makeOkResponse(401));
|
|
439
|
+
|
|
440
|
+
const agent = createAgent();
|
|
441
|
+
const result = await agent.run();
|
|
442
|
+
|
|
443
|
+
const happyFinding = result.findings.find(f => f.id.includes('happy'));
|
|
444
|
+
expect(happyFinding).toBeDefined();
|
|
445
|
+
expect(happyFinding!.severity).toBe('high');
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('does not create a finding for 401 or 404 in happy path', async () => {
|
|
449
|
+
readdirMock.mockResolvedValue(['clients.routes.ts']);
|
|
450
|
+
readFileMock.mockResolvedValue(`router.get('/clients', authenticate, getClients);`);
|
|
451
|
+
|
|
452
|
+
const authedInstance = {
|
|
453
|
+
get: vi.fn().mockResolvedValue(makeOkResponse(404)),
|
|
454
|
+
post: postMock,
|
|
455
|
+
put: putMock,
|
|
456
|
+
delete: deleteMock,
|
|
457
|
+
};
|
|
458
|
+
createAuthenticatedMock.mockResolvedValue(authedInstance);
|
|
459
|
+
getMock.mockResolvedValue(makeOkResponse(401));
|
|
460
|
+
|
|
461
|
+
const agent = createAgent();
|
|
462
|
+
const result = await agent.run();
|
|
463
|
+
|
|
464
|
+
const happyFindings = result.findings.filter(f => f.id.includes('happy'));
|
|
465
|
+
expect(happyFindings).toHaveLength(0);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// -------------------------------------------------------------------------
|
|
470
|
+
// No environment
|
|
471
|
+
// -------------------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
describe('no environment', () => {
|
|
474
|
+
it('creates a critical finding when no environment is configured', async () => {
|
|
475
|
+
const noEnvConfig: ValidatedConfig = {
|
|
476
|
+
...stubConfig,
|
|
477
|
+
environments: {},
|
|
478
|
+
};
|
|
479
|
+
fetchMock.mockRejectedValue(new Error('No env'));
|
|
480
|
+
|
|
481
|
+
const agent = new ApiExerciserAgent(noEnvConfig, 'beta', '/tmp/test-run');
|
|
482
|
+
const result = await agent.run();
|
|
483
|
+
|
|
484
|
+
expect(result.status).toBe('failed');
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// -------------------------------------------------------------------------
|
|
489
|
+
// Auth client creation failure
|
|
490
|
+
// -------------------------------------------------------------------------
|
|
491
|
+
|
|
492
|
+
describe('auth client creation failure', () => {
|
|
493
|
+
it('still runs auth checks with anonymous client when createAuthenticated fails', async () => {
|
|
494
|
+
readdirMock.mockResolvedValue(['clients.routes.ts']);
|
|
495
|
+
readFileMock.mockResolvedValue(`router.get('/clients', authenticate, getClients);`);
|
|
496
|
+
|
|
497
|
+
createAuthenticatedMock.mockRejectedValue(new Error('Auth failed'));
|
|
498
|
+
getMock.mockResolvedValue(makeOkResponse(401));
|
|
499
|
+
|
|
500
|
+
const agent = createAgent();
|
|
501
|
+
const result = await agent.run();
|
|
502
|
+
|
|
503
|
+
// Auth check still runs (anonymous client returns 401 — no finding)
|
|
504
|
+
expect(getMock).toHaveBeenCalled();
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// -------------------------------------------------------------------------
|
|
509
|
+
// PUT validation check
|
|
510
|
+
// -------------------------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
describe('PUT validation check', () => {
|
|
513
|
+
it('runs validation check for PUT endpoints with empty body', async () => {
|
|
514
|
+
readdirMock.mockResolvedValue(['clients.routes.ts']);
|
|
515
|
+
readFileMock.mockResolvedValue(`router.put('/clients/:id', authenticate, updateClient);`);
|
|
516
|
+
|
|
517
|
+
const authedInstance = {
|
|
518
|
+
get: getMock,
|
|
519
|
+
post: postMock,
|
|
520
|
+
put: vi.fn().mockResolvedValue(makeOkResponse(200)),
|
|
521
|
+
delete: deleteMock,
|
|
522
|
+
};
|
|
523
|
+
createAuthenticatedMock.mockResolvedValue(authedInstance);
|
|
524
|
+
putMock.mockResolvedValue(makeOkResponse(200)); // anon also gets 200
|
|
525
|
+
|
|
526
|
+
const agent = createAgent();
|
|
527
|
+
const result = await agent.run();
|
|
528
|
+
|
|
529
|
+
const validationFinding = result.findings.find(f =>
|
|
530
|
+
f.id.includes('validation') && f.description.includes('empty body'),
|
|
531
|
+
);
|
|
532
|
+
expect(validationFinding).toBeDefined();
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// -------------------------------------------------------------------------
|
|
537
|
+
// NestJS @Put/@Delete decorators
|
|
538
|
+
// -------------------------------------------------------------------------
|
|
539
|
+
|
|
540
|
+
describe('NestJS decorator patterns', () => {
|
|
541
|
+
it('discovers NestJS @Put decorator patterns', () => {
|
|
542
|
+
const agent = createAgent();
|
|
543
|
+
const content = `@Put('/invoices/:id') update() {}`;
|
|
544
|
+
const endpoints = agent.parseRouteFile(content, 'invoices.controller.ts');
|
|
545
|
+
expect(endpoints.find(e => e.method === 'PUT')).toBeDefined();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
it('discovers NestJS @Delete decorator patterns', () => {
|
|
549
|
+
const agent = createAgent();
|
|
550
|
+
const content = `@Delete('/invoices/:id') remove() {}`;
|
|
551
|
+
const endpoints = agent.parseRouteFile(content, 'invoices.controller.ts');
|
|
552
|
+
expect(endpoints.find(e => e.method === 'DELETE')).toBeDefined();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('handles empty path in NestJS decorators', () => {
|
|
556
|
+
const agent = createAgent();
|
|
557
|
+
const content = `@Get('') findAll() {}`;
|
|
558
|
+
const endpoints = agent.parseRouteFile(content, 'root.controller.ts');
|
|
559
|
+
expect(endpoints.find(e => e.method === 'GET' && e.path === '/')).toBeDefined();
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// -------------------------------------------------------------------------
|
|
564
|
+
// Path normalization
|
|
565
|
+
// -------------------------------------------------------------------------
|
|
566
|
+
|
|
567
|
+
describe('path normalization', () => {
|
|
568
|
+
it('replaces :param path parameters with placeholder', () => {
|
|
569
|
+
const agent = createAgent();
|
|
570
|
+
const content = `router.get('/clients/:clientId/invoices/:invoiceId', h);`;
|
|
571
|
+
const endpoints = agent.parseRouteFile(content, 'routes.ts');
|
|
572
|
+
// Verify normalization works through the happy path
|
|
573
|
+
expect(endpoints[0].path).toBe('/clients/:clientId/invoices/:invoiceId');
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// -------------------------------------------------------------------------
|
|
578
|
+
// readFile failure
|
|
579
|
+
// -------------------------------------------------------------------------
|
|
580
|
+
|
|
581
|
+
describe('readFile failure', () => {
|
|
582
|
+
it('skips route files that fail to read', async () => {
|
|
583
|
+
readdirMock.mockResolvedValue(['broken.routes.ts']);
|
|
584
|
+
readFileMock.mockRejectedValue(new Error('Permission denied'));
|
|
585
|
+
|
|
586
|
+
const agent = createAgent();
|
|
587
|
+
const result = await agent.run();
|
|
588
|
+
|
|
589
|
+
// Should complete without findings
|
|
590
|
+
expect(result.status).toBe('passed');
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// -------------------------------------------------------------------------
|
|
595
|
+
// Public route /health skip
|
|
596
|
+
// -------------------------------------------------------------------------
|
|
597
|
+
|
|
598
|
+
describe('public routes', () => {
|
|
599
|
+
it('skips auth check finding for /health endpoints', async () => {
|
|
600
|
+
readdirMock.mockResolvedValue(['health.routes.ts']);
|
|
601
|
+
readFileMock.mockResolvedValue(`router.get('/health/status', healthHandler);`);
|
|
602
|
+
|
|
603
|
+
const authedInstance = {
|
|
604
|
+
get: vi.fn().mockResolvedValue(makeOkResponse(200)),
|
|
605
|
+
post: postMock,
|
|
606
|
+
put: putMock,
|
|
607
|
+
delete: deleteMock,
|
|
608
|
+
};
|
|
609
|
+
createAuthenticatedMock.mockResolvedValue(authedInstance);
|
|
610
|
+
getMock.mockResolvedValue(makeOkResponse(200));
|
|
611
|
+
|
|
612
|
+
const agent = createAgent();
|
|
613
|
+
const result = await agent.run();
|
|
614
|
+
|
|
615
|
+
const authFindings = result.findings.filter(f => f.type === 'code-bug-security');
|
|
616
|
+
expect(authFindings).toHaveLength(0);
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
});
|