/**
 * Tests for CycloneDX SBOM generation
 */
import { describe, it, expect, vi, beforeEach } from "vitest";
import { generateSBOM, generateSBOMJson, generateSBOMSummary, findingsToVulnerabilities, calculateSha256, } from "./cyclonedx.js";
// Mock fs/promises
vi.mock("fs/promises", () => ({
    readFile: vi.fn(),
    access: vi.fn(),
}));
import { readFile, access } from "fs/promises";
describe("CycloneDX SBOM", () => {
    const mockPackageJson = JSON.stringify({
        name: "test-project",
        version: "1.0.0",
        description: "A test project",
        author: "Test Author",
        license: "MIT",
        dependencies: {
            express: "^4.18.0",
            lodash: "^4.17.21",
        },
        devDependencies: {
            vitest: "^1.0.0",
        },
    });
    const mockPackageLock = JSON.stringify({
        name: "test-project",
        version: "1.0.0",
        lockfileVersion: 3,
        packages: {
            "": {
                name: "test-project",
                version: "1.0.0",
                dependencies: {
                    express: "^4.18.0",
                    lodash: "^4.17.21",
                },
                devDependencies: {
                    vitest: "^1.0.0",
                },
            },
            "node_modules/express": {
                version: "4.18.2",
                resolved: "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
                integrity: "sha512-abc123...",
            },
            "node_modules/lodash": {
                version: "4.17.21",
                resolved: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
                integrity: "sha512-xyz789...",
            },
            "node_modules/vitest": {
                version: "1.0.4",
                resolved: "https://registry.npmjs.org/vitest/-/vitest-1.0.4.tgz",
                integrity: "sha512-dev000...",
                dev: true,
            },
        },
    });
    beforeEach(() => {
        vi.clearAllMocks();
    });
    describe("generateSBOM", () => {
        it("generates SBOM from package.json and lock file", async () => {
            vi.mocked(access).mockResolvedValue(undefined);
            vi.mocked(readFile).mockImplementation(async (path) => {
                if (String(path).includes("package-lock.json")) {
                    return mockPackageLock;
                }
                return mockPackageJson;
            });
            const sbom = await generateSBOM("/test/path");
            expect(sbom.bomFormat).toBe("CycloneDX");
            expect(sbom.specVersion).toBe("1.5");
            expect(sbom.serialNumber).toMatch(/^urn:uuid:/);
            expect(sbom.version).toBe(1);
            expect(sbom.metadata).toBeDefined();
            expect(sbom.metadata?.component?.name).toBe("test-project");
            expect(sbom.components).toBeDefined();
            expect(sbom.components.length).toBe(2); // Only prod deps by default
        });
        it("includes dev dependencies when requested", async () => {
            vi.mocked(access).mockResolvedValue(undefined);
            vi.mocked(readFile).mockImplementation(async (path) => {
                if (String(path).includes("package-lock.json")) {
                    return mockPackageLock;
                }
                return mockPackageJson;
            });
            const sbom = await generateSBOM("/test/path", { includeDevDependencies: true });
            expect(sbom.components.length).toBe(3); // Includes vitest
            const vitest = sbom.components.find((c) => c.name === "vitest");
            expect(vitest).toBeDefined();
            expect(vitest?.scope).toBe("optional");
        });
        it("generates purl for components", async () => {
            vi.mocked(access).mockResolvedValue(undefined);
            vi.mocked(readFile).mockImplementation(async (path) => {
                if (String(path).includes("package-lock.json")) {
                    return mockPackageLock;
                }
                return mockPackageJson;
            });
            const sbom = await generateSBOM("/test/path");
            const express = sbom.components.find((c) => c.name === "express");
            expect(express?.purl).toBe("pkg:npm/express@4.18.2");
        });
        it("generates bom-ref for components", async () => {
            vi.mocked(access).mockResolvedValue(undefined);
            vi.mocked(readFile).mockImplementation(async (path) => {
                if (String(path).includes("package-lock.json")) {
                    return mockPackageLock;
                }
                return mockPackageJson;
            });
            const sbom = await generateSBOM("/test/path");
            const express = sbom.components.find((c) => c.name === "express");
            expect(express?.["bom-ref"]).toBe("express@4.18.2");
        });
        it("falls back to package.json when no lock file", async () => {
            vi.mocked(access).mockImplementation(async (path) => {
                if (String(path).includes("package-lock.json")) {
                    throw new Error("Not found");
                }
                return undefined;
            });
            vi.mocked(readFile).mockResolvedValue(mockPackageJson);
            const sbom = await generateSBOM("/test/path");
            expect(sbom.components.length).toBe(2);
            // Version from package.json has ^ stripped
            const express = sbom.components.find((c) => c.name === "express");
            expect(express?.version).toBe("4.18.0");
        });
        it("throws when no package.json", async () => {
            vi.mocked(access).mockRejectedValue(new Error("Not found"));
            await expect(generateSBOM("/test/path")).rejects.toThrow("No package.json");
        });
        it("includes metadata tools", async () => {
            vi.mocked(access).mockResolvedValue(undefined);
            vi.mocked(readFile).mockImplementation(async (path) => {
                if (String(path).includes("package-lock.json")) {
                    return mockPackageLock;
                }
                return mockPackageJson;
            });
            const sbom = await generateSBOM("/test/path");
            expect(sbom.metadata?.tools).toBeDefined();
            expect(sbom.metadata?.tools?.[0].name).toBe("vaspera-hardening");
        });
        it("generates dependencies graph", async () => {
            vi.mocked(access).mockResolvedValue(undefined);
            vi.mocked(readFile).mockImplementation(async (path) => {
                if (String(path).includes("package-lock.json")) {
                    return mockPackageLock;
                }
                return mockPackageJson;
            });
            const sbom = await generateSBOM("/test/path");
            expect(sbom.dependencies).toBeDefined();
            expect(sbom.dependencies.length).toBeGreaterThan(0);
        });
    });
    describe("generateSBOMJson", () => {
        it("returns valid JSON string", async () => {
            vi.mocked(access).mockResolvedValue(undefined);
            vi.mocked(readFile).mockImplementation(async (path) => {
                if (String(path).includes("package-lock.json")) {
                    return mockPackageLock;
                }
                return mockPackageJson;
            });
            const json = await generateSBOMJson("/test/path");
            expect(() => JSON.parse(json)).not.toThrow();
            const parsed = JSON.parse(json);
            expect(parsed.bomFormat).toBe("CycloneDX");
        });
    });
    describe("generateSBOMSummary", () => {
        it("generates compact summary", async () => {
            vi.mocked(access).mockResolvedValue(undefined);
            vi.mocked(readFile).mockImplementation(async (path) => {
                if (String(path).includes("package-lock.json")) {
                    return mockPackageLock;
                }
                return mockPackageJson;
            });
            const sbom = await generateSBOM("/test/path");
            const summary = generateSBOMSummary(sbom);
            expect(summary).toContain("test-project");
            expect(summary).toContain("1.0.0");
            expect(summary).toContain("2 components");
        });
    });
    describe("findingsToVulnerabilities", () => {
        it("converts scanner findings to vulnerabilities", () => {
            const findings = [
                {
                    scanner: "npm-audit",
                    ruleId: "GHSA-123",
                    file: "package.json",
                    line: 1,
                    message: "Vulnerable dependency",
                    severity: "high",
                    confidence: 100,
                    cweIds: ["CWE-400"],
                    cveIds: ["CVE-2023-1234"],
                },
            ];
            const vulns = findingsToVulnerabilities(findings);
            expect(vulns.length).toBe(1);
            expect(vulns[0].id).toBe("CVE-2023-1234");
            expect(vulns[0].ratings?.[0].severity).toBe("high");
            expect(vulns[0].cwes).toContain(400);
        });
        it("uses ruleId when no CVE", () => {
            const findings = [
                {
                    scanner: "npm-audit",
                    ruleId: "GHSA-xyz",
                    file: "package.json",
                    line: 1,
                    message: "Advisory",
                    severity: "medium",
                    confidence: 100,
                },
            ];
            const vulns = findingsToVulnerabilities(findings);
            expect(vulns[0].id).toBe("GHSA-xyz");
        });
    });
    describe("calculateSha256", () => {
        it("calculates SHA-256 hash", () => {
            const hash = calculateSha256("test content");
            expect(hash).toBe("6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72");
        });
        it("produces different hashes for different content", () => {
            const hash1 = calculateSha256("content1");
            const hash2 = calculateSha256("content2");
            expect(hash1).not.toBe(hash2);
        });
    });
});
//# sourceMappingURL=cyclonedx.test.js.map