Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] fix: deserialization of XML/JSON #538

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

from os import getenv, path
from os.path import join
from typing import TYPE_CHECKING, Any, Generator, Iterable, List, Optional, TypeVar, Union
from typing import TYPE_CHECKING, Any, Dict, Generator, Iterable, List, Optional, Set, TypeVar, Union
from unittest import TestCase
from uuid import UUID

Expand All @@ -37,6 +37,14 @@
OWN_DATA_DIRECTORY = path.join(_TESTDATA_DIRECTORY, 'own')
SNAPSHOTS_DIRECTORY = path.join(_TESTDATA_DIRECTORY, 'snapshots')

UNDEFINED_SCHEMA_VERSIONS: Dict[OutputFormat, Set[SchemaVersion]] = {
OutputFormat.XML: set(),
OutputFormat.JSON: {SchemaVersion.V1_0, SchemaVersion.V1_1, },
}

LATEST_SUPPORTED_SCHEMA = SchemaVersion.V1_5


RECREATE_SNAPSHOTS = '1' == getenv('CDX_TEST_RECREATE_SNAPSHOTS')
if RECREATE_SNAPSHOTS:
print('!!! WILL RECREATE ALL SNAPSHOTS !!!')
Expand Down
26 changes: 23 additions & 3 deletions tests/test_deserialize_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.


from glob import iglob
from json import loads as json_loads
from os.path import join
from os.path import basename, join
from typing import Any, Callable
from unittest import TestCase
from unittest.mock import patch
Expand All @@ -27,9 +28,19 @@
from cyclonedx.model.bom import Bom
from cyclonedx.model.license import DisjunctiveLicense, LicenseExpression, LicenseRepository
from cyclonedx.schema import OutputFormat, SchemaVersion
from tests import OWN_DATA_DIRECTORY, DeepCompareMixin, SnapshotMixin, mksname
from tests import (
LATEST_SUPPORTED_SCHEMA,
OWN_DATA_DIRECTORY,
SCHEMA_TESTDATA_DIRECTORY,
UNDEFINED_SCHEMA_VERSIONS,
DeepCompareMixin,
SnapshotMixin,
mksname,
)
from tests._data.models import all_get_bom_funct_valid_immut, all_get_bom_funct_with_incomplete_deps

_UNDEFINED_SCHEMA_VERSIONS = UNDEFINED_SCHEMA_VERSIONS[OutputFormat.JSON]


@ddt
class TestDeserializeJson(TestCase, SnapshotMixin, DeepCompareMixin):
Expand All @@ -38,7 +49,7 @@ class TestDeserializeJson(TestCase, SnapshotMixin, DeepCompareMixin):
@patch('cyclonedx.model.ThisTool._version', 'TESTING')
def test_prepared(self, get_bom: Callable[[], Bom], *_: Any, **__: Any) -> None:
# only latest schema will have all data populated in serialized form
snapshot_name = mksname(get_bom, SchemaVersion.V1_5, OutputFormat.JSON)
snapshot_name = mksname(get_bom, LATEST_SUPPORTED_SCHEMA, OutputFormat.JSON)
expected = get_bom()
json = json_loads(self.readSnapshot(snapshot_name))
bom = Bom.from_json(json)
Expand Down Expand Up @@ -71,3 +82,12 @@ def test(ls: LicenseRepository) -> None:
test(bom.metadata.component.licenses)
test(list(bom.components)[0].licenses)
test(list(bom.services)[0].licenses)

@named_data(*(
(f'{sv.name}/{basename(tf)}', tf)
for sv in SchemaVersion if sv not in _UNDEFINED_SCHEMA_VERSIONS
for tf in iglob(join(SCHEMA_TESTDATA_DIRECTORY, sv.to_version(), f'valid-*.json'))
))
def test_schemaTestData(self, tf: str) -> None:
with open(tf, 'r') as s:
Bom.from_json(json_loads(s.read()))
26 changes: 24 additions & 2 deletions tests/test_deserialize_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.


from glob import iglob
from os.path import basename, join
from typing import Any, Callable
from unittest import TestCase
from unittest.mock import patch
Expand All @@ -24,9 +26,20 @@

from cyclonedx.model.bom import Bom
from cyclonedx.schema import OutputFormat, SchemaVersion
from tests import DeepCompareMixin, SnapshotMixin, mksname
from tests import (
LATEST_SUPPORTED_SCHEMA,
SCHEMA_TESTDATA_DIRECTORY,
UNDEFINED_SCHEMA_VERSIONS,
DeepCompareMixin,
SnapshotMixin,
mksname,
)
from tests._data.models import all_get_bom_funct_valid_immut, all_get_bom_funct_with_incomplete_deps

# only latest schema will have all data populated in serialized form

_UNDEFINED_SCHEMA_VERSIONS = UNDEFINED_SCHEMA_VERSIONS[OutputFormat.XML]


@ddt
class TestDeserializeXml(TestCase, SnapshotMixin, DeepCompareMixin):
Expand All @@ -35,9 +48,18 @@ class TestDeserializeXml(TestCase, SnapshotMixin, DeepCompareMixin):
@patch('cyclonedx.model.ThisTool._version', 'TESTING')
def test_prepared(self, get_bom: Callable[[], Bom], *_: Any, **__: Any) -> None:
# only latest schema will have all data populated in serialized form
snapshot_name = mksname(get_bom, SchemaVersion.V1_5, OutputFormat.XML)
snapshot_name = mksname(get_bom, LATEST_SUPPORTED_SCHEMA, OutputFormat.XML)
expected = get_bom()
with open(self.getSnapshotFile(snapshot_name), 'r') as s:
bom = Bom.from_xml(s)
self.assertBomDeepEqual(expected, bom,
fuzzy_deps=get_bom in all_get_bom_funct_with_incomplete_deps)

@named_data(*(
(f'{sv.name}/{basename(tf)}', tf)
for sv in SchemaVersion if sv not in _UNDEFINED_SCHEMA_VERSIONS
for tf in iglob(join(SCHEMA_TESTDATA_DIRECTORY, sv.to_version(), f'valid-*.xml'))
))
def test_schemaTestData(self, tf: str) -> None:
with open(tf, 'r') as s:
Bom.from_xml(s)
9 changes: 2 additions & 7 deletions tests/test_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
from cyclonedx.schema import OutputFormat, SchemaVersion
from cyclonedx.schema._res import BOM_JSON as SCHEMA_JSON, BOM_XML as SCHEMA_XML
from cyclonedx.validation import make_schemabased_validator
from tests import SnapshotMixin
from tests import UNDEFINED_SCHEMA_VERSIONS, SnapshotMixin
from tests._data.models import _make_bom

# region SUT: all the enums
Expand Down Expand Up @@ -111,16 +111,11 @@ def dp_cases_from_json_schemas(*jsonpointer: str) -> Generator[str, None, None]:
yield from dp_cases_from_json_schema(sf, jsonpointer)


UNSUPPORTED_OF_SV = frozenset([
(OutputFormat.JSON, SchemaVersion.V1_1),
(OutputFormat.JSON, SchemaVersion.V1_0),
])

NAMED_OF_SV = tuple(
(f'{of.name}-{sv.to_version()}', of, sv)
for of in OutputFormat
for sv in SchemaVersion
if (of, sv) not in UNSUPPORTED_OF_SV
if sv not in UNDEFINED_SCHEMA_VERSIONS.get(of, ())
)


Expand Down
10 changes: 5 additions & 5 deletions tests/test_output_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@
from cyclonedx.output.json import BY_SCHEMA_VERSION, Json
from cyclonedx.schema import OutputFormat, SchemaVersion
from cyclonedx.validation.json import JsonStrictValidator
from tests import SnapshotMixin, mksname
from tests import UNDEFINED_SCHEMA_VERSIONS, SnapshotMixin, mksname
from tests._data.models import all_get_bom_funct_invalid, all_get_bom_funct_valid, bom_all_same_bomref

UNSUPPORTED_SV = frozenset((SchemaVersion.V1_1, SchemaVersion.V1_0,))
_UNDEFINED_SCHEMA_VERSIONS = UNDEFINED_SCHEMA_VERSIONS[OutputFormat.JSON]


@ddt
class TestOutputJson(TestCase, SnapshotMixin):

@data(*UNSUPPORTED_SV)
@data(*_UNDEFINED_SCHEMA_VERSIONS)
def test_unsupported_schema_raises(self, sv: SchemaVersion) -> None:
outputter_class = BY_SCHEMA_VERSION[sv]
self.assertTrue(issubclass(outputter_class, Json))
Expand All @@ -51,7 +51,7 @@ def test_unsupported_schema_raises(self, sv: SchemaVersion) -> None:
@named_data(*((f'{n}-{sv.to_version()}', gb, sv)
for n, gb in all_get_bom_funct_valid
for sv in SchemaVersion
if sv not in UNSUPPORTED_SV))
if sv not in _UNDEFINED_SCHEMA_VERSIONS))
@unpack
@patch('cyclonedx.model.ThisTool._version', 'TESTING')
def test_valid(self, get_bom: Callable[[], Bom], sv: SchemaVersion, *_: Any, **__: Any) -> None:
Expand All @@ -70,7 +70,7 @@ def test_valid(self, get_bom: Callable[[], Bom], sv: SchemaVersion, *_: Any, **_
@named_data(*((f'{n}-{sv.to_version()}', gb, sv)
for n, gb in all_get_bom_funct_invalid
for sv in SchemaVersion
if sv not in UNSUPPORTED_SV))
if sv not in _UNDEFINED_SCHEMA_VERSIONS))
@unpack
def test_invalid(self, get_bom: Callable[[], Bom], sv: SchemaVersion) -> None:
bom = get_bom()
Expand Down
15 changes: 13 additions & 2 deletions tests/test_output_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,32 @@
from unittest.mock import Mock, patch
from warnings import warn

from ddt import ddt, idata, named_data, unpack
from ddt import data, ddt, idata, named_data, unpack

from cyclonedx.exception import CycloneDxException, MissingOptionalDependencyException
from cyclonedx.exception.model import LicenseExpressionAlongWithOthersException, UnknownComponentDependencyException
from cyclonedx.exception.output import FormatNotSupportedException
from cyclonedx.model.bom import Bom
from cyclonedx.output.xml import BY_SCHEMA_VERSION, Xml
from cyclonedx.schema import OutputFormat, SchemaVersion
from cyclonedx.validation.xml import XmlValidator
from tests import SnapshotMixin, mksname
from tests import UNDEFINED_SCHEMA_VERSIONS, SnapshotMixin, mksname
from tests._data.models import all_get_bom_funct_invalid, all_get_bom_funct_valid, bom_all_same_bomref

_UNDEFINED_SCHEMA_VERSIONS = UNDEFINED_SCHEMA_VERSIONS[OutputFormat.XML]


@ddt
class TestOutputXml(TestCase, SnapshotMixin):

@data(*_UNDEFINED_SCHEMA_VERSIONS)
def test_unsupported_schema_raises(self, sv: SchemaVersion) -> None:
outputter_class = BY_SCHEMA_VERSION[sv]
self.assertTrue(issubclass(outputter_class, Xml))
outputter = outputter_class(Mock(spec=Bom))
with self.assertRaises(FormatNotSupportedException):
outputter.output_as_string()

@named_data(*(
(f'{n}-{sv.to_version()}', gb, sv) for n, gb in all_get_bom_funct_valid for sv in SchemaVersion
))
Expand Down
19 changes: 9 additions & 10 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,17 @@

from cyclonedx.schema import OutputFormat, SchemaVersion
from cyclonedx.validation import make_schemabased_validator

UNDEFINED_FORMAT_VERSION = {
(OutputFormat.JSON, SchemaVersion.V1_1),
(OutputFormat.JSON, SchemaVersion.V1_0),
}
from tests import UNDEFINED_SCHEMA_VERSIONS


@ddt
class TestGetSchemabasedValidator(TestCase):

@named_data(*([f'{f.name} {v.name}', f, v]
for f, v
in product(OutputFormat, SchemaVersion)
if (f, v) not in UNDEFINED_FORMAT_VERSION))
@named_data(
*([f'{of.name} {sv.name}', of, sv]
for of, sv in product(OutputFormat, SchemaVersion)
if sv not in UNDEFINED_SCHEMA_VERSIONS.get(of, ()))
)
@unpack
def test_as_expected(self, of: OutputFormat, sv: SchemaVersion) -> None:
validator = make_schemabased_validator(of, sv)
Expand All @@ -46,7 +43,9 @@ def test_as_expected(self, of: OutputFormat, sv: SchemaVersion) -> None:

@data(
*(('foo', sv, (ValueError, 'Unexpected output_format')) for sv in SchemaVersion),
*((f, v, (ValueError, 'Unsupported schema_version')) for f, v in UNDEFINED_FORMAT_VERSION)
*((of, sv, (ValueError, 'Unsupported schema_version'))
for of in UNDEFINED_SCHEMA_VERSIONS
for sv in UNDEFINED_SCHEMA_VERSIONS[of])
)
@unpack
def test_fails_on_wrong_args(self, of: OutputFormat, sv: SchemaVersion, raises_regex: Tuple) -> None:
Expand Down
12 changes: 6 additions & 6 deletions tests/test_validation_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,28 @@
from cyclonedx.exception import MissingOptionalDependencyException
from cyclonedx.schema import OutputFormat, SchemaVersion
from cyclonedx.validation.json import JsonStrictValidator, JsonValidator
from tests import SCHEMA_TESTDATA_DIRECTORY
from tests import SCHEMA_TESTDATA_DIRECTORY, UNDEFINED_SCHEMA_VERSIONS

UNSUPPORTED_SCHEMA_VERSIONS = {SchemaVersion.V1_0, SchemaVersion.V1_1, }
_UNDEFINED_SCHEMA_VERSIONS = UNDEFINED_SCHEMA_VERSIONS[OutputFormat.JSON]


def _dp(prefix: str) -> Generator:
return (
(sv, tf) for sv in SchemaVersion if sv not in UNSUPPORTED_SCHEMA_VERSIONS
(sv, tf) for sv in SchemaVersion if sv not in _UNDEFINED_SCHEMA_VERSIONS
for tf in iglob(join(SCHEMA_TESTDATA_DIRECTORY, sv.to_version(), f'{prefix}-*.json'))
)


@ddt
class TestJsonValidator(TestCase):

@idata(sv for sv in SchemaVersion if sv not in UNSUPPORTED_SCHEMA_VERSIONS)
@idata(sv for sv in SchemaVersion if sv not in _UNDEFINED_SCHEMA_VERSIONS)
def test_validator_as_expected(self, schema_version: SchemaVersion) -> None:
validator = JsonValidator(schema_version)
self.assertIs(validator.schema_version, schema_version)
self.assertIs(validator.output_format, OutputFormat.JSON)

@idata(UNSUPPORTED_SCHEMA_VERSIONS)
@idata(_UNDEFINED_SCHEMA_VERSIONS)
def test_throws_with_unsupported_schema_version(self, schema_version: SchemaVersion) -> None:
with self.assertRaisesRegex(ValueError, 'Unsupported schema_version'):
JsonValidator(schema_version)
Expand Down Expand Up @@ -80,7 +80,7 @@ def test_validate_expected_error(self, schema_version: SchemaVersion, test_data_
@ddt
class TestJsonStrictValidator(TestCase):

@data(*UNSUPPORTED_SCHEMA_VERSIONS)
@data(*_UNDEFINED_SCHEMA_VERSIONS)
def test_throws_with_unsupported_schema_version(self, schema_version: SchemaVersion) -> None:
with self.assertRaisesRegex(ValueError, 'Unsupported schema_version'):
JsonStrictValidator(schema_version)
Expand Down
10 changes: 5 additions & 5 deletions tests/test_validation_xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,28 @@
from cyclonedx.exception import MissingOptionalDependencyException
from cyclonedx.schema import OutputFormat, SchemaVersion
from cyclonedx.validation.xml import XmlValidator
from tests import SCHEMA_TESTDATA_DIRECTORY
from tests import SCHEMA_TESTDATA_DIRECTORY, UNDEFINED_SCHEMA_VERSIONS

UNSUPPORTED_SCHEMA_VERSIONS = set()
_UNDEFINED_SCHEMA_VERSIONS = UNDEFINED_SCHEMA_VERSIONS[OutputFormat.XML]


def _dp(prefix: str) -> Generator:
return (
(sv, tf) for sv in SchemaVersion if sv not in UNSUPPORTED_SCHEMA_VERSIONS
(sv, tf) for sv in SchemaVersion if sv not in _UNDEFINED_SCHEMA_VERSIONS
for tf in iglob(join(SCHEMA_TESTDATA_DIRECTORY, sv.to_version(), f'{prefix}-*.xml'))
)


@ddt
class TestXmlValidator(TestCase):

@idata(sv for sv in SchemaVersion if sv not in UNSUPPORTED_SCHEMA_VERSIONS)
@idata(sv for sv in SchemaVersion if sv not in _UNDEFINED_SCHEMA_VERSIONS)
def test_validator_as_expected(self, schema_version: SchemaVersion) -> None:
validator = XmlValidator(schema_version)
self.assertIs(validator.schema_version, schema_version)
self.assertIs(validator.output_format, OutputFormat.XML)

@idata(UNSUPPORTED_SCHEMA_VERSIONS)
@idata(_UNDEFINED_SCHEMA_VERSIONS)
def test_throws_with_unsupported_schema_version(self, schema_version: SchemaVersion) -> None:
with self.assertRaisesRegex(ValueError, f'unsupported schema_version: {schema_version}'):
XmlValidator(schema_version)
Expand Down
Loading