/**
 * Unit tests for scanner aggregator module
 */
import { describe, it, expect, beforeEach, afterEach, afterAll } from "vitest";
import { mkdir, rm, writeFile } from "fs/promises";
import { join } from "path";
import { tmpdir } from "os";
import { runAllScanners, checkScannersAvailable, scannerFindingsToCertificationFindings, generateScannerSummary, } from "./index.js";
const TEST_BASE = join(tmpdir(), "vaspera-aggregator-test-" + Date.now() + "-" + Math.random().toString(36).slice(2));
let testProjectPath;
beforeEach(async () => {
    testProjectPath = join(TEST_BASE, "project-" + Math.random().toString(36).slice(2));
    await mkdir(testProjectPath, { recursive: true });
});
afterEach(async () => {
    try {
        await rm(testProjectPath, { recursive: true, force: true });
    }
    catch {
        // Ignore cleanup errors
    }
});
afterAll(async () => {
    try {
        await rm(TEST_BASE, { recursive: true, force: true });
    }
    catch {
        // Ignore cleanup errors
    }
});
describe("checkScannersAvailable", () => {
    it("returns availability for all scanner types", { timeout: 30000 }, async () => {
        const availability = await checkScannersAvailable();
        expect(availability).toHaveProperty("npm-audit");
        expect(availability).toHaveProperty("semgrep");
        expect(availability).toHaveProperty("gitleaks");
        expect(availability).toHaveProperty("tsc");
        expect(availability).toHaveProperty("eslint");
        // Each should have available boolean
        for (const scanner of Object.values(availability)) {
            expect(scanner).toHaveProperty("available");
            expect(typeof scanner.available).toBe("boolean");
        }
    });
    it("reports TypeScript as available", { timeout: 30000 }, async () => {
        const availability = await checkScannersAvailable();
        expect(availability.tsc.available).toBe(true);
        expect(availability.tsc.version).toBeDefined();
    });
});
describe("runAllScanners", () => {
    it("returns aggregated result structure", async () => {
        await writeFile(join(testProjectPath, "index.ts"), `export const x = 1;`);
        await writeFile(join(testProjectPath, "package.json"), JSON.stringify({ name: "test", version: "1.0.0" }));
        const result = await runAllScanners(testProjectPath, {
            semgrep: false, // Skip semgrep for faster test
            dependencies: false, // Skip npm audit (needs package-lock)
            secrets: true,
            typescript: true,
        });
        expect(result).toHaveProperty("timestamp");
        expect(result).toHaveProperty("projectPath", testProjectPath);
        expect(result).toHaveProperty("scanners");
        expect(result).toHaveProperty("totalFindings");
        expect(result).toHaveProperty("bySeverity");
        expect(result).toHaveProperty("byScanner");
        expect(result).toHaveProperty("totalDuration");
        expect(result).toHaveProperty("allSucceeded");
        expect(result).toHaveProperty("failedScanners");
        expect(Array.isArray(result.scanners)).toBe(true);
        expect(typeof result.totalFindings).toBe("number");
        expect(typeof result.totalDuration).toBe("number");
    });
    it("counts findings by severity", async () => {
        await writeFile(join(testProjectPath, "index.ts"), `export const x = 1;`);
        const result = await runAllScanners(testProjectPath, {
            semgrep: false,
            dependencies: false,
            secrets: true,
            typescript: true,
        });
        expect(result.bySeverity).toHaveProperty("critical");
        expect(result.bySeverity).toHaveProperty("high");
        expect(result.bySeverity).toHaveProperty("medium");
        expect(result.bySeverity).toHaveProperty("low");
        expect(result.bySeverity).toHaveProperty("info");
        // Total should match sum of severities
        const severitySum = result.bySeverity.critical +
            result.bySeverity.high +
            result.bySeverity.medium +
            result.bySeverity.low +
            result.bySeverity.info;
        expect(result.totalFindings).toBe(severitySum);
    });
    it("counts findings by scanner", async () => {
        await writeFile(join(testProjectPath, "index.ts"), `export const x = 1;`);
        const result = await runAllScanners(testProjectPath, {
            semgrep: false,
            dependencies: false,
            secrets: true,
            typescript: true,
        });
        expect(result.byScanner).toHaveProperty("semgrep");
        expect(result.byScanner).toHaveProperty("npm-audit");
        expect(result.byScanner).toHaveProperty("gitleaks");
        expect(result.byScanner).toHaveProperty("tsc");
        expect(result.byScanner).toHaveProperty("eslint");
    });
    it("respects disabled scanners", async () => {
        await writeFile(join(testProjectPath, "index.ts"), `export const x = 1;`);
        const result = await runAllScanners(testProjectPath, {
            semgrep: false,
            dependencies: false,
            secrets: false,
            typescript: false,
        });
        expect(result.scanners).toHaveLength(0);
        expect(result.totalFindings).toBe(0);
        expect(result.allSucceeded).toBe(true);
    });
    it("tracks failed scanners", async () => {
        // Non-existent path will cause scanners to fail
        const result = await runAllScanners("/nonexistent/project/path", {
            semgrep: false,
            dependencies: false,
            secrets: true,
            typescript: true,
        });
        // Some scanners may fail on non-existent path
        expect(Array.isArray(result.failedScanners)).toBe(true);
    });
    it("runs enabled scanners in parallel", async () => {
        await writeFile(join(testProjectPath, "index.ts"), `export const x = 1;`);
        const startTime = Date.now();
        const result = await runAllScanners(testProjectPath, {
            semgrep: false,
            dependencies: false,
            secrets: true,
            typescript: true,
        });
        const elapsed = Date.now() - startTime;
        // Duration should be roughly max of individual scanners, not sum
        // (indicating parallel execution)
        expect(result.totalDuration).toBeLessThanOrEqual(elapsed + 100);
        expect(result.scanners.length).toBe(2); // secrets + typescript
    });
});
describe("scannerFindingsToCertificationFindings", () => {
    it("converts scanner findings to certification format", () => {
        const scanResult = {
            timestamp: new Date().toISOString(),
            projectPath: "/test",
            scanners: [
                {
                    scanner: "gitleaks",
                    findings: [
                        {
                            scanner: "gitleaks",
                            ruleId: "gitleaks:aws-access-key",
                            file: "config.ts",
                            line: 5,
                            message: "AWS Access Key",
                            severity: "critical",
                            confidence: 100,
                        },
                    ],
                    duration: 100,
                    success: true,
                },
                {
                    scanner: "tsc",
                    findings: [
                        {
                            scanner: "tsc",
                            ruleId: "tsc:any-usage",
                            file: "index.ts",
                            line: 10,
                            message: "Implicit any type",
                            severity: "medium",
                            confidence: 100,
                        },
                    ],
                    duration: 50,
                    success: true,
                },
            ],
            totalFindings: 2,
            bySeverity: { critical: 1, high: 0, medium: 1, low: 0, info: 0 },
            byScanner: { semgrep: 0, "npm-audit": 0, gitleaks: 1, tsc: 1, eslint: 0, bandit: 0, gosec: 0, brakeman: 0, trivy: 0, "binary-analysis": 0, "memory-safety": 0, "race-condition": 0, healthcare: 0, plugin: 0, logic: 0, dast: 0, zap: 0, nuclei: 0, terraform: 0, tfsec: 0, checkov: 0, openapi: 0, spectral: 0, rust: 0, "cargo-audit": 0, clippy: 0, detection: 0 },
            totalDuration: 150,
            allSucceeded: true,
            failedScanners: [],
        };
        const certFindings = scannerFindingsToCertificationFindings(scanResult);
        expect(certFindings).toHaveLength(2);
        // Check gitleaks finding
        const secretFinding = certFindings.find((f) => f.scanner_source === "gitleaks");
        expect(secretFinding).toBeDefined();
        expect(secretFinding.id).toBe("sec-001");
        expect(secretFinding.category).toBe("secrets");
        expect(secretFinding.confidence).toBe(100);
        expect(secretFinding.severity).toBe("critical");
        // Check tsc finding
        const tscFinding = certFindings.find((f) => f.scanner_source === "tsc");
        expect(tscFinding).toBeDefined();
        expect(tscFinding.id).toBe("tsc-001");
        expect(tscFinding.category).toBe("type-safety");
        expect(tscFinding.confidence).toBe(100);
    });
    it("assigns correct categories based on scanner", () => {
        const scanResult = {
            timestamp: new Date().toISOString(),
            projectPath: "/test",
            scanners: [
                {
                    scanner: "npm-audit",
                    findings: [
                        {
                            scanner: "npm-audit",
                            ruleId: "npm-audit:lodash",
                            file: "package.json",
                            line: 1,
                            message: "Prototype Pollution in lodash",
                            severity: "high",
                            confidence: 100,
                        },
                    ],
                    duration: 100,
                    success: true,
                },
            ],
            totalFindings: 1,
            bySeverity: { critical: 0, high: 1, medium: 0, low: 0, info: 0 },
            byScanner: { semgrep: 0, "npm-audit": 1, gitleaks: 0, tsc: 0, eslint: 0, bandit: 0, gosec: 0, brakeman: 0, trivy: 0, "binary-analysis": 0, "memory-safety": 0, "race-condition": 0, healthcare: 0, plugin: 0, logic: 0, dast: 0, zap: 0, nuclei: 0, terraform: 0, tfsec: 0, checkov: 0, openapi: 0, spectral: 0, rust: 0, "cargo-audit": 0, clippy: 0, detection: 0 },
            totalDuration: 100,
            allSucceeded: true,
            failedScanners: [],
        };
        const certFindings = scannerFindingsToCertificationFindings(scanResult);
        expect(certFindings[0].category).toBe("dependency-vulnerability");
        expect(certFindings[0].id).toBe("dep-001");
    });
    it("preserves file and line information", () => {
        const scanResult = {
            timestamp: new Date().toISOString(),
            projectPath: "/test",
            scanners: [
                {
                    scanner: "semgrep",
                    findings: [
                        {
                            scanner: "semgrep",
                            ruleId: "semgrep:owasp.sql-injection",
                            file: "src/db.ts",
                            line: 42,
                            message: "SQL injection risk",
                            severity: "high",
                            confidence: 100,
                            cweIds: ["CWE-89"],
                        },
                    ],
                    duration: 200,
                    success: true,
                },
            ],
            totalFindings: 1,
            bySeverity: { critical: 0, high: 1, medium: 0, low: 0, info: 0 },
            byScanner: { semgrep: 1, "npm-audit": 0, gitleaks: 0, tsc: 0, eslint: 0, bandit: 0, gosec: 0, brakeman: 0, trivy: 0, "binary-analysis": 0, "memory-safety": 0, "race-condition": 0, healthcare: 0, plugin: 0, logic: 0, dast: 0, zap: 0, nuclei: 0, terraform: 0, tfsec: 0, checkov: 0, openapi: 0, spectral: 0, rust: 0, "cargo-audit": 0, clippy: 0, detection: 0 },
            totalDuration: 200,
            allSucceeded: true,
            failedScanners: [],
        };
        const certFindings = scannerFindingsToCertificationFindings(scanResult);
        expect(certFindings[0].file).toBe("src/db.ts");
        expect(certFindings[0].line).toBe(42);
        expect(certFindings[0].description).toBe("SQL injection risk");
        expect(certFindings[0].scanner_rule_id).toBe("semgrep:owasp.sql-injection");
    });
});
describe("generateScannerSummary", () => {
    it("generates markdown summary", () => {
        const scanResult = {
            timestamp: "2024-01-01T00:00:00.000Z",
            projectPath: "/test/project",
            scanners: [
                {
                    scanner: "gitleaks",
                    findings: [
                        {
                            scanner: "gitleaks",
                            ruleId: "gitleaks:aws-key",
                            file: "config.ts",
                            line: 1,
                            message: "AWS Key detected",
                            severity: "critical",
                            confidence: 100,
                        },
                    ],
                    duration: 100,
                    success: true,
                },
                {
                    scanner: "tsc",
                    findings: [],
                    duration: 50,
                    success: true,
                },
            ],
            totalFindings: 1,
            bySeverity: { critical: 1, high: 0, medium: 0, low: 0, info: 0 },
            byScanner: { semgrep: 0, "npm-audit": 0, gitleaks: 1, tsc: 0, eslint: 0, bandit: 0, gosec: 0, brakeman: 0, trivy: 0, "binary-analysis": 0, "memory-safety": 0, "race-condition": 0, healthcare: 0, plugin: 0, logic: 0, dast: 0, zap: 0, nuclei: 0, terraform: 0, tfsec: 0, checkov: 0, openapi: 0, spectral: 0, rust: 0, "cargo-audit": 0, clippy: 0, detection: 0 },
            totalDuration: 150,
            allSucceeded: true,
            failedScanners: [],
        };
        const summary = generateScannerSummary(scanResult);
        expect(summary).toContain("# Deterministic Scanner Results");
        expect(summary).toContain("/test/project");
        expect(summary).toContain("| Critical | 1 |");
        expect(summary).toContain("| gitleaks |");
        expect(summary).toContain("| tsc |");
        expect(summary).toContain("**Total** | **1**");
    });
    it("includes top findings for critical/high", () => {
        const scanResult = {
            timestamp: "2024-01-01T00:00:00.000Z",
            projectPath: "/test",
            scanners: [
                {
                    scanner: "semgrep",
                    findings: [
                        {
                            scanner: "semgrep",
                            ruleId: "semgrep:sql-injection",
                            file: "db.ts",
                            line: 10,
                            message: "SQL injection vulnerability",
                            severity: "critical",
                            confidence: 100,
                        },
                        {
                            scanner: "semgrep",
                            ruleId: "semgrep:xss",
                            file: "view.ts",
                            line: 20,
                            message: "XSS vulnerability",
                            severity: "high",
                            confidence: 100,
                        },
                    ],
                    duration: 100,
                    success: true,
                },
            ],
            totalFindings: 2,
            bySeverity: { critical: 1, high: 1, medium: 0, low: 0, info: 0 },
            byScanner: { semgrep: 2, "npm-audit": 0, gitleaks: 0, tsc: 0, eslint: 0, bandit: 0, gosec: 0, brakeman: 0, trivy: 0, "binary-analysis": 0, "memory-safety": 0, "race-condition": 0, healthcare: 0, plugin: 0, logic: 0, dast: 0, zap: 0, nuclei: 0, terraform: 0, tfsec: 0, checkov: 0, openapi: 0, spectral: 0, rust: 0, "cargo-audit": 0, clippy: 0, detection: 0 },
            totalDuration: 100,
            allSucceeded: true,
            failedScanners: [],
        };
        const summary = generateScannerSummary(scanResult);
        expect(summary).toContain("## Top Findings");
        expect(summary).toContain("[CRITICAL]");
        expect(summary).toContain("[HIGH]");
        expect(summary).toContain("db.ts:10");
        expect(summary).toContain("view.ts:20");
    });
    it("shows scanner status indicators", () => {
        const scanResult = {
            timestamp: "2024-01-01T00:00:00.000Z",
            projectPath: "/test",
            scanners: [
                {
                    scanner: "semgrep",
                    findings: [],
                    duration: 100,
                    success: true,
                },
                {
                    scanner: "npm-audit",
                    findings: [],
                    duration: 50,
                    success: false,
                    error: "npm audit failed: no package-lock.json",
                },
            ],
            totalFindings: 0,
            bySeverity: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
            byScanner: { semgrep: 0, "npm-audit": 0, gitleaks: 0, tsc: 0, eslint: 0, bandit: 0, gosec: 0, brakeman: 0, trivy: 0, "binary-analysis": 0, "memory-safety": 0, "race-condition": 0, healthcare: 0, plugin: 0, logic: 0, dast: 0, zap: 0, nuclei: 0, terraform: 0, tfsec: 0, checkov: 0, openapi: 0, spectral: 0, rust: 0, "cargo-audit": 0, clippy: 0, detection: 0 },
            totalDuration: 150,
            allSucceeded: false,
            failedScanners: ["npm-audit"],
        };
        const summary = generateScannerSummary(scanResult);
        expect(summary).toContain("✅"); // Success indicator
        expect(summary).toContain("❌"); // Failure indicator
        expect(summary).toContain("npm audit failed");
    });
});
describe("deduplication", () => {
    it("deduplicates findings from multiple scanners", async () => {
        // This would require multiple scanners to report the same issue
        // which is hard to simulate in unit tests
        // The logic is tested implicitly through aggregation
        await writeFile(join(testProjectPath, "index.ts"), `export const x: any = 1;`);
        const result = await runAllScanners(testProjectPath, {
            semgrep: false,
            dependencies: false,
            secrets: false,
            typescript: true,
        });
        // Each unique file:line:message should appear only once
        const seen = new Set();
        for (const scanner of result.scanners) {
            for (const finding of scanner.findings) {
                const key = `${finding.file}:${finding.line}:${finding.message.slice(0, 50)}`;
                expect(seen.has(key)).toBe(false); // No duplicates within a scanner
                seen.add(key);
            }
        }
    });
});
//# sourceMappingURL=index.test.js.map