182 lines
5.2 KiB
TypeScript
182 lines
5.2 KiB
TypeScript
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/)
|
|
})
|
|
})
|
|
})
|