|
22 | 22 | from cyclonedx.model.component import Component |
23 | 23 | from cyclonedx.model.dependency import Dependency |
24 | 24 | from cyclonedx.model.license import DisjunctiveLicense, LicenseExpression |
25 | | -from cyclonedx.validation.model import ModelValidator |
| 25 | +from cyclonedx.validation.model import ModelValidationErrorSeverity, ModelValidator |
26 | 26 |
|
27 | 27 |
|
28 | 28 | class TestModelValidator(TestCase): |
29 | | - def test_validate_multiple_errors(self) -> None: |
| 29 | + |
| 30 | + def test_validate_clean_bom(self) -> None: |
| 31 | + bom = Bom() |
| 32 | + bom.metadata.component = Component(name='root', version='1.0', bom_ref='root') |
| 33 | + errors = list(ModelValidator().validate(bom)) |
| 34 | + self.assertEqual(0, len(errors)) |
| 35 | + |
| 36 | + def test_validate_multiple_errors_have_error_severity(self) -> None: |
30 | 37 | bom = Bom() |
31 | | - # Error 1: Component with multiple licenses including expression |
32 | 38 | comp = Component(name='test', version='1.0', bom_ref='test-comp') |
33 | 39 | comp.licenses.update([ |
34 | 40 | DisjunctiveLicense(id='MIT'), |
35 | | - LicenseExpression(value='Apache-2.0 OR MIT') |
| 41 | + LicenseExpression(value='Apache-2.0 OR MIT'), |
36 | 42 | ]) |
37 | 43 | bom.components.add(comp) |
38 | | - |
39 | | - # Error 2: Unknown dependency reference |
40 | 44 | bom.dependencies.add(Dependency('test-comp', dependencies=[Dependency('non-existent-ref')])) |
41 | 45 |
|
42 | | - validator = ModelValidator() |
43 | | - errors = list(validator.validate(bom)) |
| 46 | + errors = list(ModelValidator().validate(bom)) |
44 | 47 |
|
45 | | - self.assertEqual(len(errors), 2) |
| 48 | + self.assertEqual(2, len(errors)) |
46 | 49 | error_types = [type(e.data) for e in errors] |
47 | 50 | self.assertIn(UnknownComponentDependencyException, error_types) |
48 | 51 | self.assertIn(LicenseExpressionAlongWithOthersException, error_types) |
| 52 | + for error in errors: |
| 53 | + self.assertEqual(ModelValidationErrorSeverity.ERROR, error.severity) |
49 | 54 |
|
50 | | - def test_validate_clean_bom(self) -> None: |
| 55 | + def test_validate_unknown_toplevel_dependency_ref_detected(self) -> None: |
| 56 | + """Regression: top-level d.ref values must also be validated against known BOM components.""" |
51 | 57 | bom = Bom() |
52 | | - bom.metadata.component = Component(name='root', version='1.0', bom_ref='root') |
53 | | - validator = ModelValidator() |
54 | | - errors = list(validator.validate(bom)) |
55 | | - self.assertEqual(len(errors), 0) |
| 58 | + comp = Component(name='real', version='1.0', bom_ref='real-comp') |
| 59 | + bom.components.add(comp) |
| 60 | + # 'ghost-ref' is not in the BOM at all |
| 61 | + bom.dependencies.add(Dependency('ghost-ref')) |
56 | 62 |
|
57 | | - def test_bom_validate_deprecated_behavior(self) -> None: |
| 63 | + errors = list(ModelValidator().validate(bom)) |
| 64 | + |
| 65 | + error_types = [type(e.data) for e in errors] |
| 66 | + self.assertIn(UnknownComponentDependencyException, error_types) |
| 67 | + |
| 68 | + def test_validate_incomplete_dependency_graph_yields_warning(self) -> None: |
| 69 | + """Check #2 must yield a WARNING-severity error, not a Python UserWarning.""" |
| 70 | + import warnings as _warnings |
58 | 71 | bom = Bom() |
59 | 72 | bom.metadata.component = Component(name='root', version='1.0', bom_ref='root') |
| 73 | + bom.components.add(Component(name='dep', version='1.0', bom_ref='dep')) |
| 74 | + |
| 75 | + with _warnings.catch_warnings(): |
| 76 | + _warnings.simplefilter('error') # turn any Python warning into an error |
| 77 | + errors = list(ModelValidator().validate(bom)) # must not raise |
60 | 78 |
|
61 | | - # Verify side effect: register_dependency is called by Bom.validate |
62 | | - self.assertEqual(len(bom.dependencies), 0) |
63 | | - with self.assertWarns(DeprecationWarning): |
64 | | - bom.validate() |
65 | | - self.assertEqual(len(bom.dependencies), 1) |
66 | | - self.assertEqual(next(iter(bom.dependencies)).ref.value, 'root') |
| 79 | + warning_errors = [e for e in errors if e.severity == ModelValidationErrorSeverity.WARNING] |
| 80 | + self.assertEqual(1, len(warning_errors)) |
| 81 | + self.assertIsInstance(warning_errors[0].data, UserWarning) |
67 | 82 |
|
68 | | - def test_model_validator_no_side_effects(self) -> None: |
| 83 | + def test_validate_nested_root_component_license_invalid(self) -> None: |
| 84 | + """Regression: nested components under metadata.component must be license-checked.""" |
69 | 85 | bom = Bom() |
70 | | - bom.metadata.component = Component(name='root', version='1.0', bom_ref='root') |
| 86 | + root = Component(name='root', version='1.0', bom_ref='root') |
| 87 | + nested = Component(name='nested', version='1.0', bom_ref='nested') |
| 88 | + nested.licenses.update([ |
| 89 | + DisjunctiveLicense(id='MIT'), |
| 90 | + LicenseExpression(value='Apache-2.0 OR MIT'), |
| 91 | + ]) |
| 92 | + root.components.add(nested) |
| 93 | + bom.metadata.component = root |
| 94 | + |
| 95 | + errors = list(ModelValidator().validate(bom)) |
71 | 96 |
|
72 | | - # Verify NO side effect: ModelValidator should not call register_dependency |
73 | | - self.assertEqual(len(bom.dependencies), 0) |
74 | | - validator = ModelValidator() |
75 | | - list(validator.validate(bom)) |
76 | | - self.assertEqual(len(bom.dependencies), 0) |
| 97 | + error_types = [type(e.data) for e in errors] |
| 98 | + self.assertIn(LicenseExpressionAlongWithOthersException, error_types) |
| 99 | + |
| 100 | + def test_validate_no_side_effects(self) -> None: |
| 101 | + bom = Bom() |
| 102 | + bom.metadata.component = Component(name='root', version='1.0', bom_ref='root') |
| 103 | + self.assertEqual(0, len(bom.dependencies)) |
| 104 | + list(ModelValidator().validate(bom)) |
| 105 | + self.assertEqual(0, len(bom.dependencies)) |
0 commit comments