import { jest } from '@jest/globals' import fs from 'fs/promises' import os from 'os' import path from 'path' import { parseSBOMFromPath, generateSBOMPredicate, SBOM } from '../../src/sbom' describe('parseSBOMFromPath', () => { let tempDir: string beforeEach(async () => { tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sbom-test-')) }) afterEach(async () => { await fs.rm(tempDir, { recursive: true, force: true }) }) describe('file handling', () => { it('should throw when file does not exist', async () => { await expect(parseSBOMFromPath('/nonexistent/file.json')).rejects.toThrow( /SBOM file not found/ ) }) it('should rethrow non-ENOENT errors', async () => { const statSpy = jest.spyOn(fs, 'stat').mockRejectedValueOnce( Object.assign(new Error('Permission denied'), { code: 'EACCES' }) ) await expect(parseSBOMFromPath('/some/file.json')).rejects.toThrow( /Permission denied/ ) statSpy.mockRestore() }) it('should throw when file contains invalid JSON', async () => { const filePath = path.join(tempDir, 'invalid.json') await fs.writeFile(filePath, 'not valid json') await expect(parseSBOMFromPath(filePath)).rejects.toThrow(/JSON/) }) it('should throw when file exceeds maximum size', async () => { const filePath = path.join(tempDir, 'large.json') const largeContent = 'x'.repeat(17 * 1024 * 1024) await fs.writeFile(filePath, largeContent) await expect(parseSBOMFromPath(filePath)).rejects.toThrow( /SBOM file exceeds maximum allowed size/ ) }) }) describe('SPDX format', () => { const spdxSBOM = { spdxVersion: 'SPDX-2.3', SPDXID: 'SPDXRef-DOCUMENT', name: 'test-package', packages: [] } it('should parse valid SPDX SBOM', async () => { const filePath = path.join(tempDir, 'sbom.spdx.json') await fs.writeFile(filePath, JSON.stringify(spdxSBOM)) const result = await parseSBOMFromPath(filePath) expect(result.type).toBe('spdx') expect(result.object).toEqual(spdxSBOM) }) }) describe('CycloneDX format', () => { const cyclonedxSBOM = { bomFormat: 'CycloneDX', specVersion: '1.4', serialNumber: 'urn:uuid:12345', components: [] } it('should parse valid CycloneDX SBOM', async () => { const filePath = path.join(tempDir, 'sbom.cdx.json') await fs.writeFile(filePath, JSON.stringify(cyclonedxSBOM)) const result = await parseSBOMFromPath(filePath) expect(result.type).toBe('cyclonedx') expect(result.object).toEqual(cyclonedxSBOM) }) }) describe('unsupported formats', () => { it('should throw for unrecognized SBOM format', async () => { const filePath = path.join(tempDir, 'invalid-sbom.json') await fs.writeFile(filePath, JSON.stringify({ random: 'data' })) await expect(parseSBOMFromPath(filePath)).rejects.toThrow( /Unsupported SBOM format/ ) }) it('should throw for SPDX missing SPDXID', async () => { const filePath = path.join(tempDir, 'partial-spdx.json') await fs.writeFile(filePath, JSON.stringify({ spdxVersion: 'SPDX-2.3' })) await expect(parseSBOMFromPath(filePath)).rejects.toThrow( /Unsupported SBOM format/ ) }) it('should throw for CycloneDX missing required fields', async () => { const filePath = path.join(tempDir, 'partial-cdx.json') await fs.writeFile(filePath, JSON.stringify({ bomFormat: 'CycloneDX' })) await expect(parseSBOMFromPath(filePath)).rejects.toThrow( /Unsupported SBOM format/ ) }) }) }) describe('generateSBOMPredicate', () => { describe('SPDX predicates', () => { it('should generate predicate with correct SPDX type URL', () => { const sbom: SBOM = { type: 'spdx', object: { spdxVersion: 'SPDX-2.3', SPDXID: 'SPDXRef-DOCUMENT', name: 'test-package' } } const predicate = generateSBOMPredicate(sbom) expect(predicate.type).toBe('https://spdx.dev/Document/v2.3') expect(predicate.params).toEqual(sbom.object) }) it('should throw when spdxVersion is missing', () => { const sbom: SBOM = { type: 'spdx', object: { SPDXID: 'SPDXRef-DOCUMENT' } } expect(() => generateSBOMPredicate(sbom)).toThrow( /Cannot find spdxVersion/ ) }) }) describe('CycloneDX predicates', () => { it('should generate predicate with correct CycloneDX type URL', () => { const sbom: SBOM = { type: 'cyclonedx', object: { bomFormat: 'CycloneDX', specVersion: '1.4', serialNumber: 'urn:uuid:12345' } } const predicate = generateSBOMPredicate(sbom) expect(predicate.type).toBe('https://cyclonedx.org/bom') expect(predicate.params).toEqual(sbom.object) }) }) describe('unsupported types', () => { it('should throw for unsupported SBOM type', () => { const sbom = { type: 'unknown' as SBOM['type'], object: { foo: 'bar' } } expect(() => generateSBOMPredicate(sbom)).toThrow(/Unsupported SBOM format/) }) }) })