Skip to content

Commit 439246e

Browse files
authored
fix: improve basic SPDX assertion and add tests (#10)
1 parent f672d78 commit 439246e

File tree

2 files changed

+400
-1
lines changed

2 files changed

+400
-1
lines changed

src/bom/validation.test.ts

Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
import { test, describe } from 'node:test';
2+
import { strict as assert } from 'node:assert';
3+
import { isCdxBom, isSpdxBom, isSupportedBom } from './validation.ts';
4+
import type { CdxBom, Component } from '../types/index.js';
5+
import type { SPDX23 } from '../types/bom/spdx-2.3.schema.ts';
6+
7+
function createValidCdxBom(overrides: Partial<CdxBom> = {}): CdxBom {
8+
return {
9+
bomFormat: 'CycloneDX',
10+
specVersion: '1.5',
11+
version: 1,
12+
components: [],
13+
...overrides,
14+
};
15+
}
16+
17+
function createValidSpdxBom(overrides: Partial<SPDX23> = {}): SPDX23 {
18+
return {
19+
SPDXID: 'SPDXRef-DOCUMENT',
20+
spdxVersion: 'SPDX-2.3',
21+
dataLicense: 'CC0-1.0',
22+
name: 'test-document',
23+
creationInfo: {
24+
created: '2024-01-01T00:00:00Z',
25+
creators: ['Tool: test-tool-1.0'],
26+
},
27+
...overrides,
28+
};
29+
}
30+
31+
describe('BOM Validation', () => {
32+
describe('isCdxBom', () => {
33+
test('should identify valid CDX BOM object', () => {
34+
const validBom = createValidCdxBom();
35+
assert.equal(isCdxBom(validBom), true);
36+
});
37+
38+
test('should identify valid CDX BOM from JSON string', () => {
39+
const validBom = createValidCdxBom();
40+
const jsonString = JSON.stringify(validBom);
41+
assert.equal(isCdxBom(jsonString), true);
42+
});
43+
44+
test('should identify complex CDX BOM with metadata and components', () => {
45+
const complexBom = createValidCdxBom({
46+
metadata: {
47+
timestamp: '2024-01-20T10:30:00.000Z',
48+
tools: [{ name: 'cyclonedx-bom', version: '3.11.7' }],
49+
},
50+
components: [
51+
{
52+
type: 'library',
53+
'bom-ref': 'pkg:npm/[email protected]',
54+
name: 'lodash',
55+
version: '4.17.21',
56+
} as Component,
57+
],
58+
});
59+
assert.equal(isCdxBom(complexBom), true);
60+
});
61+
62+
test('should reject BOM missing bomFormat', () => {
63+
const invalidBom = { ...createValidCdxBom() };
64+
delete (invalidBom as any).bomFormat;
65+
assert.equal(isCdxBom(invalidBom), false);
66+
});
67+
68+
test('should reject BOM with wrong bomFormat', () => {
69+
const invalidBom = createValidCdxBom({ bomFormat: 'SPDX' as any });
70+
assert.equal(isCdxBom(invalidBom), false);
71+
});
72+
73+
test('should reject BOM missing components', () => {
74+
const invalidBom = { ...createValidCdxBom() };
75+
delete (invalidBom as any).components;
76+
assert.equal(isCdxBom(invalidBom), false);
77+
});
78+
79+
test('should reject malformed JSON string', () => {
80+
const malformedJson = '{"bomFormat": "CycloneDX", "components": [}';
81+
assert.equal(isCdxBom(malformedJson), false);
82+
});
83+
84+
test('should reject empty string', () => {
85+
assert.equal(isCdxBom(''), false);
86+
});
87+
88+
test('should reject null', () => {
89+
assert.equal(isCdxBom(null as any), false);
90+
});
91+
92+
test('should reject undefined', () => {
93+
assert.equal(isCdxBom(undefined as any), false);
94+
});
95+
96+
test('should reject non-object types', () => {
97+
assert.equal(isCdxBom(123 as any), false);
98+
assert.equal(isCdxBom(true as any), false);
99+
assert.equal(isCdxBom(['array']), false);
100+
});
101+
102+
test('should reject SPDX BOM', () => {
103+
const spdxBom = createValidSpdxBom();
104+
assert.equal(isCdxBom(spdxBom), false);
105+
});
106+
});
107+
108+
describe('isSpdxBom', () => {
109+
test('should identify valid SPDX BOM object', () => {
110+
const validBom = createValidSpdxBom();
111+
assert.equal(isSpdxBom(validBom), true);
112+
});
113+
114+
test('should identify valid SPDX BOM from JSON string', () => {
115+
const validBom = createValidSpdxBom();
116+
const jsonString = JSON.stringify(validBom);
117+
assert.equal(isSpdxBom(jsonString), true);
118+
});
119+
120+
test('should identify SPDX BOM with packages and relationships', () => {
121+
const complexBom = createValidSpdxBom({
122+
packages: [
123+
{
124+
SPDXID: 'SPDXRef-Package-lodash',
125+
name: 'lodash',
126+
downloadLocation:
127+
'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz',
128+
},
129+
],
130+
relationships: [
131+
{
132+
spdxElementId: 'SPDXRef-Package-lodash',
133+
relationshipType: 'DEPENDENCY_OF',
134+
relatedSpdxElement: 'SPDXRef-DOCUMENT',
135+
},
136+
],
137+
});
138+
assert.equal(isSpdxBom(complexBom), true);
139+
});
140+
141+
test('should reject BOM missing SPDXID', () => {
142+
const invalidBom = { ...createValidSpdxBom() };
143+
delete (invalidBom as any).SPDXID;
144+
assert.equal(isSpdxBom(invalidBom), false);
145+
});
146+
147+
test('should reject BOM with wrong SPDXID format', () => {
148+
const invalidBom = createValidSpdxBom({ SPDXID: 'SPDXRef-Package-test' });
149+
assert.equal(isSpdxBom(invalidBom), false);
150+
});
151+
152+
test('should reject BOM with wrong SPDXID value', () => {
153+
const invalidBom = createValidSpdxBom({ SPDXID: 'SPDXRef-Document' });
154+
assert.equal(isSpdxBom(invalidBom), false);
155+
});
156+
157+
test('should reject BOM missing spdxVersion', () => {
158+
const invalidBom = { ...createValidSpdxBom() };
159+
delete (invalidBom as any).spdxVersion;
160+
assert.equal(isSpdxBom(invalidBom), false);
161+
});
162+
163+
test('should reject BOM with wrong spdxVersion format', () => {
164+
const invalidBom = createValidSpdxBom({ spdxVersion: '2.3' });
165+
assert.equal(isSpdxBom(invalidBom), false);
166+
});
167+
168+
test('should reject BOM with non-string spdxVersion', () => {
169+
const invalidBom = createValidSpdxBom({ spdxVersion: 2.3 as any });
170+
assert.equal(isSpdxBom(invalidBom), false);
171+
});
172+
173+
test('should reject malformed JSON string', () => {
174+
const malformedJson =
175+
'{"SPDXID": "SPDXRef-DOCUMENT", "spdxVersion": "SPDX-2.3"';
176+
assert.equal(isSpdxBom(malformedJson), false);
177+
});
178+
179+
test('should reject empty string', () => {
180+
assert.equal(isSpdxBom(''), false);
181+
});
182+
183+
test('should reject null', () => {
184+
assert.equal(isSpdxBom(null as any), false);
185+
});
186+
187+
test('should reject undefined', () => {
188+
assert.equal(isSpdxBom(undefined as any), false);
189+
});
190+
191+
test('should reject non-object types', () => {
192+
assert.equal(isSpdxBom(123 as any), false);
193+
assert.equal(isSpdxBom(true as any), false);
194+
assert.equal(isSpdxBom(['array']), false);
195+
});
196+
197+
test('should reject CDX BOM', () => {
198+
const cdxBom = createValidCdxBom();
199+
assert.equal(isSpdxBom(cdxBom), false);
200+
});
201+
});
202+
203+
describe('isSupportedBom', () => {
204+
test('should identify valid CDX BOM as supported', () => {
205+
const cdxBom = createValidCdxBom();
206+
assert.equal(isSupportedBom(cdxBom), true);
207+
});
208+
209+
test('should identify valid SPDX BOM as supported', () => {
210+
const spdxBom = createValidSpdxBom();
211+
assert.equal(isSupportedBom(spdxBom), true);
212+
});
213+
214+
test('should identify valid CDX BOM string as supported', () => {
215+
const cdxBom = createValidCdxBom();
216+
const jsonString = JSON.stringify(cdxBom);
217+
assert.equal(isSupportedBom(jsonString), true);
218+
});
219+
220+
test('should identify valid SPDX BOM string as supported', () => {
221+
const spdxBom = createValidSpdxBom();
222+
const jsonString = JSON.stringify(spdxBom);
223+
assert.equal(isSupportedBom(jsonString), true);
224+
});
225+
226+
test('should reject invalid BOM formats', () => {
227+
const invalidBom = { format: 'unknown', version: '1.0', data: [] };
228+
assert.equal(isSupportedBom(invalidBom), false);
229+
});
230+
231+
test('should reject malformed JSON string', () => {
232+
const malformedJson = '{"invalid": "json"';
233+
assert.equal(isSupportedBom(malformedJson), false);
234+
});
235+
236+
test('should reject empty object', () => {
237+
assert.equal(isSupportedBom({}), false);
238+
});
239+
240+
test('should reject primitive types', () => {
241+
assert.equal(isSupportedBom('not json'), false);
242+
assert.equal(isSupportedBom(123 as any), false);
243+
assert.equal(isSupportedBom(true as any), false);
244+
assert.equal(isSupportedBom(null as any), false);
245+
assert.equal(isSupportedBom(undefined as any), false);
246+
});
247+
});
248+
249+
describe('Edge Cases and Error Handling', () => {
250+
test('should handle objects with circular references', () => {
251+
const circularObj: any = { bomFormat: 'CycloneDX', components: [] };
252+
circularObj.self = circularObj;
253+
254+
assert.doesNotThrow(() => isCdxBom(circularObj));
255+
assert.equal(isCdxBom(circularObj), true);
256+
});
257+
258+
test('should handle very large JSON strings', () => {
259+
const largeBom = createValidCdxBom({
260+
components: Array.from(
261+
{ length: 1000 },
262+
(_, i) =>
263+
({
264+
type: 'library' as const,
265+
'bom-ref': `pkg:npm/package-${i}@1.0.0`,
266+
name: `package-${i}`,
267+
version: '1.0.0',
268+
}) as Component,
269+
),
270+
});
271+
const largeJsonString = JSON.stringify(largeBom);
272+
273+
assert.equal(isCdxBom(largeJsonString), true);
274+
assert.equal(isSupportedBom(largeJsonString), true);
275+
});
276+
277+
test('should handle BOMs with special characters', () => {
278+
const bomWithSpecialChars = createValidCdxBom({
279+
components: [
280+
{
281+
type: 'library',
282+
'bom-ref': 'pkg:npm/@scope/[email protected]',
283+
name: '@scope/package-with-special-chars',
284+
version: '1.0.0-beta.1+build.123',
285+
} as Component,
286+
],
287+
});
288+
289+
assert.equal(isCdxBom(bomWithSpecialChars), true);
290+
});
291+
292+
test('should handle SPDX BOM with minimal required fields only', () => {
293+
const minimalSpdx = {
294+
SPDXID: 'SPDXRef-DOCUMENT',
295+
spdxVersion: 'SPDX-2.3',
296+
};
297+
298+
assert.equal(isSpdxBom(minimalSpdx), true);
299+
});
300+
301+
test('should handle CDX BOM with minimal required fields only', () => {
302+
const minimalCdx = { bomFormat: 'CycloneDX', components: [] };
303+
304+
assert.equal(isCdxBom(minimalCdx), true);
305+
});
306+
307+
test('should handle case sensitivity correctly', () => {
308+
const wrongCaseCdx = { bomformat: 'CycloneDX', components: [] };
309+
310+
const wrongCaseSpdx = {
311+
spdxid: 'SPDXRef-DOCUMENT',
312+
spdxVersion: 'SPDX-2.3',
313+
};
314+
315+
assert.equal(isCdxBom(wrongCaseCdx), false);
316+
assert.equal(isSpdxBom(wrongCaseSpdx), false);
317+
});
318+
});
319+
320+
describe('Real-world Examples', () => {
321+
test('should validate real SPDX example structure', () => {
322+
const realSpdxExample = {
323+
spdxVersion: 'SPDX-2.3',
324+
dataLicense: 'CC0-1.0',
325+
SPDXID: 'SPDXRef-DOCUMENT',
326+
name: 'Example-SPDX-Document',
327+
documentNamespace: 'https://example.com/spdx/example-project',
328+
creationInfo: {
329+
created: '2025-01-20T10:30:00Z',
330+
creators: [
331+
'Tool: example-spdx-tool-1.0.0',
332+
'Person: John Doe ([email protected])',
333+
],
334+
licenseListVersion: '3.23',
335+
},
336+
packages: [
337+
{
338+
SPDXID: 'SPDXRef-Package-lodash',
339+
name: 'lodash',
340+
downloadLocation:
341+
'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz',
342+
filesAnalyzed: false,
343+
licenseConcluded: 'MIT',
344+
licenseDeclared: 'MIT',
345+
versionInfo: '4.17.21',
346+
},
347+
],
348+
};
349+
350+
assert.equal(isSpdxBom(realSpdxExample), true);
351+
assert.equal(isSupportedBom(realSpdxExample), true);
352+
});
353+
354+
test('should validate real CDX example structure', () => {
355+
const realCdxExample = {
356+
bomFormat: 'CycloneDX',
357+
specVersion: '1.5',
358+
serialNumber: 'urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79',
359+
version: 1,
360+
metadata: {
361+
timestamp: '2024-01-20T10:30:00.000Z',
362+
tools: [
363+
{ vendor: 'CycloneDX', name: 'cyclonedx-bom', version: '3.11.7' },
364+
],
365+
component: {
366+
type: 'application',
367+
'bom-ref': 'pkg:npm/[email protected]',
368+
name: 'example-app',
369+
version: '1.0.0',
370+
purl: 'pkg:npm/[email protected]',
371+
},
372+
},
373+
components: [
374+
{
375+
type: 'library',
376+
'bom-ref': 'pkg:npm/[email protected]',
377+
name: 'lodash',
378+
version: '4.17.21',
379+
purl: 'pkg:npm/[email protected]',
380+
scope: 'required',
381+
licenses: [{ license: { id: 'MIT' } }],
382+
} as Component,
383+
],
384+
};
385+
386+
assert.equal(isCdxBom(realCdxExample), true);
387+
assert.equal(isSupportedBom(realCdxExample), true);
388+
});
389+
});
390+
});

0 commit comments

Comments
 (0)