diff --git a/src/datetime_conversions.py b/src/datetime_conversions.py index 7829693cf..62fafe866 100644 --- a/src/datetime_conversions.py +++ b/src/datetime_conversions.py @@ -10,8 +10,6 @@ # limitations under the License. from datetime import datetime -from src.parser.error import SPDXParsingError - def datetime_from_str(date_str: str) -> datetime: if not isinstance(date_str, str): diff --git a/src/document_utils.py b/src/document_utils.py new file mode 100644 index 000000000..f0fe18552 --- /dev/null +++ b/src/document_utils.py @@ -0,0 +1,20 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import List + +from src.model.document import Document + + +def get_contained_spdx_element_ids(document: Document) -> List[str]: + element_ids = [file.spdx_id for file in document.files] + element_ids.extend([package.spdx_id for package in document.packages]) + element_ids.extend([snippet.spdx_id for snippet in document.snippets]) + return element_ids diff --git a/src/jsonschema/__init__.py b/src/jsonschema/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/jsonschema/annotation_converter.py b/src/jsonschema/annotation_converter.py new file mode 100644 index 000000000..9b8905053 --- /dev/null +++ b/src/jsonschema/annotation_converter.py @@ -0,0 +1,37 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Type, Any + +from src.datetime_conversions import datetime_to_iso_string +from src.jsonschema.annotation_properties import AnnotationProperty +from src.jsonschema.converter import TypedConverter +from src.jsonschema.json_property import JsonProperty +from src.model.annotation import Annotation +from src.model.document import Document + + +class AnnotationConverter(TypedConverter[Annotation]): + def _get_property_value(self, annotation: Annotation, annotation_property: AnnotationProperty, + document: Document = None) -> Any: + if annotation_property == AnnotationProperty.ANNOTATION_DATE: + return datetime_to_iso_string(annotation.annotation_date) + elif annotation_property == AnnotationProperty.ANNOTATION_TYPE: + return annotation.annotation_type.name + elif annotation_property == AnnotationProperty.ANNOTATOR: + return annotation.annotator.to_serialized_string() + elif annotation_property == AnnotationProperty.COMMENT: + return annotation.annotation_comment + + def get_json_type(self) -> Type[JsonProperty]: + return AnnotationProperty + + def get_data_model_type(self) -> Type[Annotation]: + return Annotation diff --git a/src/jsonschema/annotation_properties.py b/src/jsonschema/annotation_properties.py new file mode 100644 index 000000000..5215a2819 --- /dev/null +++ b/src/jsonschema/annotation_properties.py @@ -0,0 +1,20 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import auto + +from src.jsonschema.json_property import JsonProperty + + +class AnnotationProperty(JsonProperty): + ANNOTATION_DATE = auto() + ANNOTATION_TYPE = auto() + ANNOTATOR = auto() + COMMENT = auto() diff --git a/src/jsonschema/checksum_converter.py b/src/jsonschema/checksum_converter.py new file mode 100644 index 000000000..162a4f174 --- /dev/null +++ b/src/jsonschema/checksum_converter.py @@ -0,0 +1,40 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Type + +from src.jsonschema.checksum_properties import ChecksumProperty +from src.jsonschema.converter import TypedConverter +from src.jsonschema.json_property import JsonProperty +from src.model.checksum import Checksum, ChecksumAlgorithm +from src.model.document import Document + + +class ChecksumConverter(TypedConverter[Checksum]): + + def get_data_model_type(self) -> Type[Checksum]: + return Checksum + + def get_json_type(self) -> Type[JsonProperty]: + return ChecksumProperty + + def _get_property_value(self, checksum: Checksum, checksum_property: ChecksumProperty, + _document: Document = None) -> str: + if checksum_property == ChecksumProperty.ALGORITHM: + return algorithm_to_json_string(checksum.algorithm) + elif checksum_property == ChecksumProperty.CHECKSUM_VALUE: + return checksum.value + + +def algorithm_to_json_string(algorithm: ChecksumAlgorithm) -> str: + name_with_dash: str = algorithm.name.replace("_", "-") + if "BLAKE2B" in name_with_dash: + return name_with_dash.replace("BLAKE2B", "BLAKE2b") + return name_with_dash diff --git a/src/jsonschema/checksum_properties.py b/src/jsonschema/checksum_properties.py new file mode 100644 index 000000000..6b974d5bb --- /dev/null +++ b/src/jsonschema/checksum_properties.py @@ -0,0 +1,18 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import auto + +from src.jsonschema.json_property import JsonProperty + + +class ChecksumProperty(JsonProperty): + ALGORITHM = auto() + CHECKSUM_VALUE = auto() diff --git a/src/jsonschema/converter.py b/src/jsonschema/converter.py new file mode 100644 index 000000000..b5dc2a3c3 --- /dev/null +++ b/src/jsonschema/converter.py @@ -0,0 +1,73 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from abc import ABC, abstractmethod +from typing import Any, Type, Dict, TypeVar, Generic + +from src.jsonschema.json_property import JsonProperty +from src.model.document import Document +from src.writer.casing_tools import snake_case_to_camel_case + +MISSING_IMPLEMENTATION_MESSAGE = "Must be implemented" + +T = TypeVar("T") + + +class TypedConverter(ABC, Generic[T]): + """ + Base class for all converters between an instance of the tools-python data model and the corresponding dictionary + representation, following the json schema. The generic type T is the type according to the data model. + Each converter has several methods: + - get_json_type and get_data_model_type: return the data model type and the corresponding JsonProperty subclass. + These methods are abstract in the base class and need to be implemented in subclasses. + - json_property_name: converts an enum value of a JsonProperty subclass to the corresponding property name in the + json schema. The default implementation simply converts from snake case to camel case. Can be overridden in case + of exceptions like "SPDXID". + - convert: converts an instance of type T (one of the data model types) to a dictionary representation. In some + cases, the full document is required (see below). The logic should be generic for all types. + - requires_full_document: indicates whether the full document is required for conversion. Returns False by + default, can be overridden as needed for specific types. + - _get_property_value: Retrieves the value of a specific json property from the data model instance. In some + cases, the full document is required. + """ + + @abstractmethod + def _get_property_value(self, instance: T, json_property: JsonProperty, document: Document = None) -> Any: + raise NotImplementedError(MISSING_IMPLEMENTATION_MESSAGE) + + @abstractmethod + def get_json_type(self) -> Type[JsonProperty]: + raise NotImplementedError(MISSING_IMPLEMENTATION_MESSAGE) + + @abstractmethod + def get_data_model_type(self) -> Type[T]: + raise NotImplementedError(MISSING_IMPLEMENTATION_MESSAGE) + + def json_property_name(self, json_property: JsonProperty) -> str: + return snake_case_to_camel_case(json_property.name) + + def requires_full_document(self) -> bool: + return False + + def convert(self, instance: T, document: Document = None) -> Dict: + if not isinstance(instance, self.get_data_model_type()): + raise TypeError( + f"Converter of type {self.__class__} can only convert objects of type " + f"{self.get_data_model_type()}. Received {type(instance)} instead.") + if self.requires_full_document() and not document: + raise ValueError(f"Converter of type {self.__class__} requires the full document") + + result = {} + for property_name in self.get_json_type(): + property_value = self._get_property_value(instance, property_name, document) + if property_value is None: + continue + result[self.json_property_name(property_name)] = property_value + return result diff --git a/src/jsonschema/creation_info_converter.py b/src/jsonschema/creation_info_converter.py new file mode 100644 index 000000000..0111a8cab --- /dev/null +++ b/src/jsonschema/creation_info_converter.py @@ -0,0 +1,37 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Type, Any + +from src.datetime_conversions import datetime_to_iso_string +from src.jsonschema.converter import TypedConverter +from src.jsonschema.creation_info_properties import CreationInfoProperty +from src.jsonschema.json_property import JsonProperty +from src.jsonschema.optional_utils import apply_if_present +from src.model.document import CreationInfo, Document + + +class CreationInfoConverter(TypedConverter[CreationInfo]): + def get_data_model_type(self) -> Type[CreationInfo]: + return CreationInfo + + def get_json_type(self) -> Type[JsonProperty]: + return CreationInfoProperty + + def _get_property_value(self, creation_info: CreationInfo, creation_info_property: CreationInfoProperty, + _document: Document = None) -> Any: + if creation_info_property == CreationInfoProperty.CREATED: + return datetime_to_iso_string(creation_info.created) + elif creation_info_property == CreationInfoProperty.CREATORS: + return [creator.to_serialized_string() for creator in creation_info.creators] or None + elif creation_info_property == CreationInfoProperty.LICENSE_LIST_VERSION: + return apply_if_present(str, creation_info.license_list_version) + elif creation_info_property == CreationInfoProperty.COMMENT: + return creation_info.creator_comment diff --git a/src/jsonschema/creation_info_properties.py b/src/jsonschema/creation_info_properties.py new file mode 100644 index 000000000..ccd52f58c --- /dev/null +++ b/src/jsonschema/creation_info_properties.py @@ -0,0 +1,20 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import auto + +from src.jsonschema.json_property import JsonProperty + + +class CreationInfoProperty(JsonProperty): + CREATED = auto() + CREATORS = auto() + LICENSE_LIST_VERSION = auto() + COMMENT = auto() diff --git a/src/jsonschema/document_converter.py b/src/jsonschema/document_converter.py new file mode 100644 index 000000000..4d669962b --- /dev/null +++ b/src/jsonschema/document_converter.py @@ -0,0 +1,118 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Type, Any + +from src.document_utils import get_contained_spdx_element_ids +from src.jsonschema.annotation_converter import AnnotationConverter +from src.jsonschema.converter import TypedConverter +from src.jsonschema.creation_info_converter import CreationInfoConverter +from src.jsonschema.document_properties import DocumentProperty +from src.jsonschema.external_document_ref_converter import ExternalDocumentRefConverter +from src.jsonschema.extracted_licensing_info_converter import ExtractedLicensingInfoConverter +from src.jsonschema.file_converter import FileConverter +from src.jsonschema.json_property import JsonProperty +from src.jsonschema.package_converter import PackageConverter +from src.jsonschema.relationship_converter import RelationshipConverter +from src.jsonschema.snippet_converter import SnippetConverter +from src.model.document import Document +from src.model.relationship import RelationshipType +from src.model.relationship_filters import filter_by_type_and_origin, filter_by_type_and_target, \ + find_package_contains_file_relationships, \ + find_file_contained_by_package_relationships + + +class DocumentConverter(TypedConverter[Document]): + creation_info_converter: CreationInfoConverter + external_document_ref_converter: ExternalDocumentRefConverter + package_converter: PackageConverter + file_converter: FileConverter + snippet_converter: SnippetConverter + annotation_converter: AnnotationConverter + relationship_converter: RelationshipConverter + extracted_licensing_info_converter: ExtractedLicensingInfoConverter + + def __init__(self): + self.external_document_ref_converter = ExternalDocumentRefConverter() + self.creation_info_converter = CreationInfoConverter() + self.package_converter = PackageConverter() + self.file_converter = FileConverter() + self.snippet_converter = SnippetConverter() + self.annotation_converter = AnnotationConverter() + self.relationship_converter = RelationshipConverter() + self.extracted_licensing_info_converter = ExtractedLicensingInfoConverter() + + def get_json_type(self) -> Type[JsonProperty]: + return DocumentProperty + + def get_data_model_type(self) -> Type[Document]: + return Document + + def json_property_name(self, document_property: DocumentProperty) -> str: + if document_property == DocumentProperty.SPDX_ID: + return "SPDXID" + return super().json_property_name(document_property) + + def _get_property_value(self, document: Document, document_property: DocumentProperty, + _document: Document = None) -> Any: + if document_property == DocumentProperty.SPDX_ID: + return document.creation_info.spdx_id + elif document_property == DocumentProperty.ANNOTATIONS: + # annotations referencing files, packages or snippets will be added to those elements directly + element_ids = get_contained_spdx_element_ids(document) + document_annotations = filter(lambda annotation: annotation.spdx_id not in element_ids, + document.annotations) + return [self.annotation_converter.convert(annotation) for annotation in document_annotations] or None + elif document_property == DocumentProperty.COMMENT: + return document.creation_info.document_comment + elif document_property == DocumentProperty.CREATION_INFO: + return self.creation_info_converter.convert(document.creation_info) + elif document_property == DocumentProperty.DATA_LICENSE: + return document.creation_info.data_license + elif document_property == DocumentProperty.EXTERNAL_DOCUMENT_REFS: + return [self.external_document_ref_converter.convert(external_document_ref) for + external_document_ref in document.creation_info.external_document_refs] or None + elif document_property == DocumentProperty.HAS_EXTRACTED_LICENSING_INFO: + return [self.extracted_licensing_info_converter.convert(licensing_info) for licensing_info in + document.extracted_licensing_info] or None + elif document_property == DocumentProperty.NAME: + return document.creation_info.name + elif document_property == DocumentProperty.SPDX_VERSION: + return document.creation_info.spdx_version + elif document_property == DocumentProperty.DOCUMENT_NAMESPACE: + return document.creation_info.document_namespace + elif document_property == DocumentProperty.DOCUMENT_DESCRIBES: + describes_ids = [relationship.related_spdx_element_id for relationship in + filter_by_type_and_origin(document.relationships, RelationshipType.DESCRIBES, + document.creation_info.spdx_id)] + described_by_ids = [relationship.spdx_element_id for relationship in + filter_by_type_and_target(document.relationships, RelationshipType.DESCRIBED_BY, + document.creation_info.spdx_id)] + return describes_ids + described_by_ids or None + elif document_property == DocumentProperty.PACKAGES: + return [self.package_converter.convert(package, document) for package in document.packages] or None + elif document_property == DocumentProperty.FILES: + return [self.file_converter.convert(file, document) for file in document.files] or None + elif document_property == DocumentProperty.SNIPPETS: + return [self.snippet_converter.convert(snippet, document) for snippet in document.snippets] or None + elif document_property == DocumentProperty.RELATIONSHIPS: + already_covered_relationships = filter_by_type_and_origin(document.relationships, + RelationshipType.DESCRIBES, + document.creation_info.spdx_id) + already_covered_relationships.extend( + filter_by_type_and_target(document.relationships, RelationshipType.DESCRIBED_BY, + document.creation_info.spdx_id)) + for package in document.packages: + already_covered_relationships.extend(find_package_contains_file_relationships(document, package)) + already_covered_relationships.extend(find_file_contained_by_package_relationships(document, package)) + relationships_to_ignore = [relationship for relationship in already_covered_relationships if + relationship.comment is None] + return [self.relationship_converter.convert(relationship) for relationship in document.relationships if + relationship not in relationships_to_ignore] or None diff --git a/src/jsonschema/document_properties.py b/src/jsonschema/document_properties.py new file mode 100644 index 000000000..e684fe80f --- /dev/null +++ b/src/jsonschema/document_properties.py @@ -0,0 +1,31 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import auto + +from src.jsonschema.json_property import JsonProperty + + +class DocumentProperty(JsonProperty): + SPDX_ID = auto() + ANNOTATIONS = auto() + COMMENT = auto() + CREATION_INFO = auto() + DATA_LICENSE = auto() + EXTERNAL_DOCUMENT_REFS = auto() + HAS_EXTRACTED_LICENSING_INFO = auto() + NAME = auto() + SPDX_VERSION = auto() + DOCUMENT_NAMESPACE = auto() + DOCUMENT_DESCRIBES = auto() + PACKAGES = auto() + FILES = auto() + SNIPPETS = auto() + RELATIONSHIPS = auto() diff --git a/src/jsonschema/external_document_ref_converter.py b/src/jsonschema/external_document_ref_converter.py new file mode 100644 index 000000000..893a24014 --- /dev/null +++ b/src/jsonschema/external_document_ref_converter.py @@ -0,0 +1,41 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Type, Any + +from src.jsonschema.checksum_converter import ChecksumConverter +from src.jsonschema.converter import TypedConverter +from src.jsonschema.external_document_ref_properties import ExternalDocumentRefProperty +from src.jsonschema.json_property import JsonProperty +from src.model.document import Document +from src.model.external_document_ref import ExternalDocumentRef + + +class ExternalDocumentRefConverter(TypedConverter[ExternalDocumentRef]): + checksum_converter: ChecksumConverter + + def __init__(self): + self.checksum_converter = ChecksumConverter() + + def _get_property_value(self, external_document_ref: ExternalDocumentRef, + external_document_ref_property: ExternalDocumentRefProperty, + _document: Document = None) -> Any: + if external_document_ref_property == ExternalDocumentRefProperty.EXTERNAL_DOCUMENT_ID: + return external_document_ref.document_ref_id + elif external_document_ref_property == ExternalDocumentRefProperty.SPDX_DOCUMENT: + return external_document_ref.document_uri + elif external_document_ref_property == ExternalDocumentRefProperty.CHECKSUM: + return self.checksum_converter.convert(external_document_ref.checksum) + + def get_json_type(self) -> Type[JsonProperty]: + return ExternalDocumentRefProperty + + def get_data_model_type(self) -> Type[ExternalDocumentRef]: + return ExternalDocumentRef diff --git a/src/jsonschema/external_document_ref_properties.py b/src/jsonschema/external_document_ref_properties.py new file mode 100644 index 000000000..fd2c1eb3a --- /dev/null +++ b/src/jsonschema/external_document_ref_properties.py @@ -0,0 +1,19 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import auto + +from src.jsonschema.json_property import JsonProperty + + +class ExternalDocumentRefProperty(JsonProperty): + EXTERNAL_DOCUMENT_ID = auto() + SPDX_DOCUMENT = auto() + CHECKSUM = auto() diff --git a/src/jsonschema/external_package_ref_converter.py b/src/jsonschema/external_package_ref_converter.py new file mode 100644 index 000000000..3993b7c88 --- /dev/null +++ b/src/jsonschema/external_package_ref_converter.py @@ -0,0 +1,36 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Type, Any + +from src.jsonschema.converter import TypedConverter +from src.jsonschema.external_package_ref_properties import ExternalPackageRefProperty +from src.jsonschema.json_property import JsonProperty +from src.model.document import Document +from src.model.package import ExternalPackageRef + + +class ExternalPackageRefConverter(TypedConverter[ExternalPackageRef]): + def _get_property_value(self, external_ref: ExternalPackageRef, external_ref_property: ExternalPackageRefProperty, + document: Document = None) -> Any: + if external_ref_property == ExternalPackageRefProperty.COMMENT: + return external_ref.comment + elif external_ref_property == ExternalPackageRefProperty.REFERENCE_CATEGORY: + return external_ref.category.name + elif external_ref_property == ExternalPackageRefProperty.REFERENCE_LOCATOR: + return external_ref.locator + elif external_ref_property == ExternalPackageRefProperty.REFERENCE_TYPE: + return external_ref.reference_type + + def get_json_type(self) -> Type[JsonProperty]: + return ExternalPackageRefProperty + + def get_data_model_type(self) -> Type[ExternalPackageRef]: + return ExternalPackageRef diff --git a/src/jsonschema/external_package_ref_properties.py b/src/jsonschema/external_package_ref_properties.py new file mode 100644 index 000000000..d59348821 --- /dev/null +++ b/src/jsonschema/external_package_ref_properties.py @@ -0,0 +1,20 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import auto + +from src.jsonschema.json_property import JsonProperty + + +class ExternalPackageRefProperty(JsonProperty): + COMMENT = auto() + REFERENCE_CATEGORY = auto() + REFERENCE_LOCATOR = auto() + REFERENCE_TYPE = auto() diff --git a/src/jsonschema/extracted_licensing_info_converter.py b/src/jsonschema/extracted_licensing_info_converter.py new file mode 100644 index 000000000..0aa4630d9 --- /dev/null +++ b/src/jsonschema/extracted_licensing_info_converter.py @@ -0,0 +1,40 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Type, Any + +from src.jsonschema.converter import TypedConverter +from src.jsonschema.extracted_licensing_info_properties import ExtractedLicensingInfoProperty +from src.jsonschema.json_property import JsonProperty +from src.jsonschema.optional_utils import apply_if_present +from src.model.document import Document +from src.model.extracted_licensing_info import ExtractedLicensingInfo + + +class ExtractedLicensingInfoConverter(TypedConverter[ExtractedLicensingInfo]): + def _get_property_value(self, extracted_licensing_info: ExtractedLicensingInfo, + extracted_licensing_info_property: ExtractedLicensingInfoProperty, + document: Document = None) -> Any: + if extracted_licensing_info_property == ExtractedLicensingInfoProperty.COMMENT: + return extracted_licensing_info.comment + elif extracted_licensing_info_property == ExtractedLicensingInfoProperty.EXTRACTED_TEXT: + return extracted_licensing_info.extracted_text + elif extracted_licensing_info_property == ExtractedLicensingInfoProperty.LICENSE_ID: + return extracted_licensing_info.license_id + elif extracted_licensing_info_property == ExtractedLicensingInfoProperty.NAME: + return apply_if_present(str, extracted_licensing_info.license_name) + elif extracted_licensing_info_property == ExtractedLicensingInfoProperty.SEE_ALSOS: + return extracted_licensing_info.cross_references or None + + def get_json_type(self) -> Type[JsonProperty]: + return ExtractedLicensingInfoProperty + + def get_data_model_type(self) -> Type[ExtractedLicensingInfo]: + return ExtractedLicensingInfo diff --git a/src/jsonschema/extracted_licensing_info_properties.py b/src/jsonschema/extracted_licensing_info_properties.py new file mode 100644 index 000000000..0bfcda02a --- /dev/null +++ b/src/jsonschema/extracted_licensing_info_properties.py @@ -0,0 +1,21 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import auto + +from src.jsonschema.json_property import JsonProperty + + +class ExtractedLicensingInfoProperty(JsonProperty): + COMMENT = auto() + EXTRACTED_TEXT = auto() + LICENSE_ID = auto() + NAME = auto() + SEE_ALSOS = auto() diff --git a/src/jsonschema/file_converter.py b/src/jsonschema/file_converter.py new file mode 100644 index 000000000..fd76eefa9 --- /dev/null +++ b/src/jsonschema/file_converter.py @@ -0,0 +1,80 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Type, Any + +from src.jsonschema.annotation_converter import AnnotationConverter +from src.jsonschema.checksum_converter import ChecksumConverter +from src.jsonschema.converter import TypedConverter +from src.jsonschema.file_properties import FileProperty +from src.jsonschema.json_property import JsonProperty +from src.jsonschema.optional_utils import apply_if_present +from src.model.document import Document +from src.model.file import File + + +class FileConverter(TypedConverter[File]): + annotation_converter: AnnotationConverter + checksum_converter: ChecksumConverter + + def __init__(self): + self.annotation_converter = AnnotationConverter() + self.checksum_converter = ChecksumConverter() + + def json_property_name(self, file_property: FileProperty) -> str: + if file_property == FileProperty.SPDX_ID: + return "SPDXID" + return super().json_property_name(file_property) + + def _get_property_value(self, file: Any, file_property: FileProperty, document: Document = None) -> Any: + if file_property == FileProperty.SPDX_ID: + return file.spdx_id + elif file_property == FileProperty.ANNOTATIONS: + file_annotations = filter(lambda annotation: annotation.spdx_id == file.spdx_id, document.annotations) + return [self.annotation_converter.convert(annotation) for annotation in file_annotations] or None + elif file_property == FileProperty.ARTIFACT_OFS: + # Deprecated property, automatically converted during parsing + pass + elif file_property == FileProperty.ATTRIBUTION_TEXTS: + return file.attribution_texts or None + elif file_property == FileProperty.CHECKSUMS: + return [self.checksum_converter.convert(checksum) for checksum in file.checksums] or None + elif file_property == FileProperty.COMMENT: + return file.comment + elif file_property == FileProperty.COPYRIGHT_TEXT: + return apply_if_present(str, file.copyright_text) + elif file_property == FileProperty.FILE_CONTRIBUTORS: + return file.contributors or None + elif file_property == FileProperty.FILE_DEPENDENCIES: + # Deprecated property, automatically converted during parsing + pass + elif file_property == FileProperty.FILE_NAME: + return file.name + elif file_property == FileProperty.FILE_TYPES: + return [file_type.name for file_type in file.file_type] or None + elif file_property == FileProperty.LICENSE_COMMENTS: + return file.license_comment + elif file_property == FileProperty.LICENSE_CONCLUDED: + return apply_if_present(str, file.concluded_license) + elif file_property == FileProperty.LICENSE_INFO_IN_FILES: + if isinstance(file.license_info_in_file, list): + return [str(license_expression) for license_expression in file.license_info_in_file] or None + return apply_if_present(str, file.license_info_in_file) + elif file_property == FileProperty.NOTICE_TEXT: + return file.notice + + def get_json_type(self) -> Type[JsonProperty]: + return FileProperty + + def get_data_model_type(self) -> Type[File]: + return File + + def requires_full_document(self) -> bool: + return True diff --git a/src/jsonschema/file_properties.py b/src/jsonschema/file_properties.py new file mode 100644 index 000000000..02cc8a25b --- /dev/null +++ b/src/jsonschema/file_properties.py @@ -0,0 +1,31 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import auto + +from src.jsonschema.json_property import JsonProperty + + +class FileProperty(JsonProperty): + SPDX_ID = auto() + ANNOTATIONS = auto() + ARTIFACT_OFS = auto() + ATTRIBUTION_TEXTS = auto() + CHECKSUMS = auto() + COMMENT = auto() + COPYRIGHT_TEXT = auto() + FILE_CONTRIBUTORS = auto() + FILE_DEPENDENCIES = auto() + FILE_NAME = auto() + FILE_TYPES = auto() + LICENSE_COMMENTS = auto() + LICENSE_CONCLUDED = auto() + LICENSE_INFO_IN_FILES = auto() + NOTICE_TEXT = auto() diff --git a/src/jsonschema/json_property.py b/src/jsonschema/json_property.py new file mode 100644 index 000000000..0f65a77f2 --- /dev/null +++ b/src/jsonschema/json_property.py @@ -0,0 +1,20 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import Enum + + +class JsonProperty(Enum): + """ + Parent class for all json property classes. Not meant to be instantiated directly, only to have a common parent + type that can be used in type hints. + In general, all the child enums list the properties of the corresponding objects from the json schema. + """ + pass diff --git a/src/jsonschema/optional_utils.py b/src/jsonschema/optional_utils.py new file mode 100644 index 000000000..d5039d3e3 --- /dev/null +++ b/src/jsonschema/optional_utils.py @@ -0,0 +1,21 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Callable, TypeVar, Optional + +T = TypeVar("T") +S = TypeVar("S") + + +def apply_if_present(function: Callable[[T], S], optional_value: Optional[T]) -> Optional[S]: + """ + Apply the passed function to the optional value if it is not None. Else returns None. + """ + return function(optional_value) if optional_value else None diff --git a/src/jsonschema/package_converter.py b/src/jsonschema/package_converter.py new file mode 100644 index 000000000..fe8a26d71 --- /dev/null +++ b/src/jsonschema/package_converter.py @@ -0,0 +1,125 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Type, Any + +from src.datetime_conversions import datetime_to_iso_string +from src.jsonschema.annotation_converter import AnnotationConverter +from src.jsonschema.checksum_converter import ChecksumConverter +from src.jsonschema.converter import TypedConverter +from src.jsonschema.external_package_ref_converter import ExternalPackageRefConverter +from src.jsonschema.json_property import JsonProperty +from src.jsonschema.optional_utils import apply_if_present +from src.jsonschema.package_properties import PackageProperty +from src.jsonschema.package_verification_code_converter import PackageVerificationCodeConverter +from src.model.actor import Actor +from src.model.document import Document +from src.model.package import Package +from src.model.relationship_filters import find_package_contains_file_relationships, \ + find_file_contained_by_package_relationships + + +class PackageConverter(TypedConverter[Package]): + annotation_converter: AnnotationConverter + checksum_converter: ChecksumConverter + external_package_ref_converter: ExternalPackageRefConverter + package_verification_code_converter: PackageVerificationCodeConverter + + def __init__(self): + self.annotation_converter = AnnotationConverter() + self.checksum_converter = ChecksumConverter() + self.external_package_ref_converter = ExternalPackageRefConverter() + self.package_verification_code_converter = PackageVerificationCodeConverter() + + def json_property_name(self, package_property: PackageProperty) -> str: + if package_property == PackageProperty.SPDX_ID: + return "SPDXID" + return super().json_property_name(package_property) + + def _get_property_value(self, package: Package, package_property: PackageProperty, + document: Document = None) -> Any: + if package_property == PackageProperty.SPDX_ID: + return package.spdx_id + elif package_property == PackageProperty.ANNOTATIONS: + package_annotations = filter(lambda annotation: annotation.spdx_id == package.spdx_id, document.annotations) + return [self.annotation_converter.convert(annotation, document) for annotation in + package_annotations] or None + elif package_property == PackageProperty.ATTRIBUTION_TEXTS: + return package.attribution_texts or None + elif package_property == PackageProperty.BUILT_DATE: + return apply_if_present(datetime_to_iso_string, package.built_date) + elif package_property == PackageProperty.CHECKSUMS: + return [self.checksum_converter.convert(checksum, document) for checksum in package.checksums] or None + elif package_property == PackageProperty.COMMENT: + return package.comment + elif package_property == PackageProperty.COPYRIGHT_TEXT: + return apply_if_present(str, package.copyright_text) + elif package_property == PackageProperty.DESCRIPTION: + return package.description + elif package_property == PackageProperty.DOWNLOAD_LOCATION: + return str(package.download_location) + elif package_property == PackageProperty.EXTERNAL_REFS: + return [self.external_package_ref_converter.convert(external_ref) for external_ref in + package.external_references] or None + elif package_property == PackageProperty.FILES_ANALYZED: + return package.files_analyzed + elif package_property == PackageProperty.HAS_FILES: + package_contains_file_ids = [relationship.related_spdx_element_id for relationship in + find_package_contains_file_relationships(document, package)] + file_contained_in_package_ids = [relationship.spdx_element_id for relationship in + find_file_contained_by_package_relationships(document, package)] + return package_contains_file_ids + file_contained_in_package_ids or None + elif package_property == PackageProperty.HOMEPAGE: + return apply_if_present(str, package.homepage) + elif package_property == PackageProperty.LICENSE_COMMENTS: + return package.license_comment + elif package_property == PackageProperty.LICENSE_CONCLUDED: + return apply_if_present(str, package.license_concluded) + elif package_property == PackageProperty.LICENSE_DECLARED: + return apply_if_present(str, package.license_declared) + elif package_property == PackageProperty.LICENSE_INFO_FROM_FILES: + if isinstance(package.license_info_from_files, list): + return [str(license_expression) for license_expression in package.license_info_from_files] or None + return apply_if_present(str, package.license_info_from_files) + elif package_property == PackageProperty.NAME: + return package.name + elif package_property == PackageProperty.ORIGINATOR: + if isinstance(package.originator, Actor): + return package.originator.to_serialized_string() + return apply_if_present(str, package.originator) + elif package_property == PackageProperty.PACKAGE_FILE_NAME: + return package.file_name + elif package_property == PackageProperty.PACKAGE_VERIFICATION_CODE: + return apply_if_present(self.package_verification_code_converter.convert, package.verification_code) + elif package_property == PackageProperty.PRIMARY_PACKAGE_PURPOSE: + return package.primary_package_purpose.name if package.primary_package_purpose is not None else None + elif package_property == PackageProperty.RELEASE_DATE: + return apply_if_present(datetime_to_iso_string, package.release_date) + elif package_property == PackageProperty.SOURCE_INFO: + return package.source_info + elif package_property == PackageProperty.SUMMARY: + return package.summary + elif package_property == PackageProperty.SUPPLIER: + if isinstance(package.supplier, Actor): + return package.supplier.to_serialized_string() + return apply_if_present(str, package.supplier) + elif package_property == PackageProperty.VALID_UNTIL_DATE: + return apply_if_present(datetime_to_iso_string, package.valid_until_date) + elif package_property == PackageProperty.VERSION_INFO: + return package.version + + def get_json_type(self) -> Type[JsonProperty]: + return PackageProperty + + def get_data_model_type(self) -> Type[Package]: + return Package + + def requires_full_document(self) -> bool: + return True diff --git a/src/jsonschema/package_properties.py b/src/jsonschema/package_properties.py new file mode 100644 index 000000000..467ef5fc1 --- /dev/null +++ b/src/jsonschema/package_properties.py @@ -0,0 +1,44 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import auto + +from src.jsonschema.json_property import JsonProperty + + +class PackageProperty(JsonProperty): + SPDX_ID = auto() + ANNOTATIONS = auto() + ATTRIBUTION_TEXTS = auto() + BUILT_DATE = auto() + CHECKSUMS = auto() + COMMENT = auto() + COPYRIGHT_TEXT = auto() + DESCRIPTION = auto() + DOWNLOAD_LOCATION = auto() + EXTERNAL_REFS = auto() + FILES_ANALYZED = auto() + HAS_FILES = auto() + HOMEPAGE = auto() + LICENSE_COMMENTS = auto() + LICENSE_CONCLUDED = auto() + LICENSE_DECLARED = auto() + LICENSE_INFO_FROM_FILES = auto() + NAME = auto() + ORIGINATOR = auto() + PACKAGE_FILE_NAME = auto() + PACKAGE_VERIFICATION_CODE = auto() + PRIMARY_PACKAGE_PURPOSE = auto() + RELEASE_DATE = auto() + SOURCE_INFO = auto() + SUMMARY = auto() + SUPPLIER = auto() + VALID_UNTIL_DATE = auto() + VERSION_INFO = auto() diff --git a/src/jsonschema/package_verification_code_converter.py b/src/jsonschema/package_verification_code_converter.py new file mode 100644 index 000000000..bb6cd7a6e --- /dev/null +++ b/src/jsonschema/package_verification_code_converter.py @@ -0,0 +1,33 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Type, Any + +from src.jsonschema.converter import TypedConverter +from src.jsonschema.json_property import JsonProperty +from src.jsonschema.package_verification_code_properties import PackageVerificationCodeProperty +from src.model.document import Document +from src.model.package import PackageVerificationCode + + +class PackageVerificationCodeConverter(TypedConverter[PackageVerificationCode]): + def _get_property_value(self, verification_code: PackageVerificationCode, + verification_code_property: PackageVerificationCodeProperty, + document: Document = None) -> Any: + if verification_code_property == PackageVerificationCodeProperty.PACKAGE_VERIFICATION_CODE_EXCLUDED_FILES: + return verification_code.excluded_files or None + elif verification_code_property == PackageVerificationCodeProperty.PACKAGE_VERIFICATION_CODE_VALUE: + return verification_code.value + + def get_json_type(self) -> Type[JsonProperty]: + return PackageVerificationCodeProperty + + def get_data_model_type(self) -> Type[PackageVerificationCode]: + return PackageVerificationCode diff --git a/src/jsonschema/package_verification_code_properties.py b/src/jsonschema/package_verification_code_properties.py new file mode 100644 index 000000000..ee202f51a --- /dev/null +++ b/src/jsonschema/package_verification_code_properties.py @@ -0,0 +1,18 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import auto + +from src.jsonschema.json_property import JsonProperty + + +class PackageVerificationCodeProperty(JsonProperty): + PACKAGE_VERIFICATION_CODE_EXCLUDED_FILES = auto() + PACKAGE_VERIFICATION_CODE_VALUE = auto() diff --git a/src/jsonschema/relationship_converter.py b/src/jsonschema/relationship_converter.py new file mode 100644 index 000000000..e5b6a36a1 --- /dev/null +++ b/src/jsonschema/relationship_converter.py @@ -0,0 +1,36 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Type, Any + +from src.jsonschema.converter import TypedConverter +from src.jsonschema.json_property import JsonProperty +from src.jsonschema.relationship_properties import RelationshipProperty +from src.model.document import Document +from src.model.relationship import Relationship + + +class RelationshipConverter(TypedConverter[Relationship]): + def _get_property_value(self, relationship: Relationship, relationship_property: RelationshipProperty, + document: Document = None) -> Any: + if relationship_property == RelationshipProperty.SPDX_ELEMENT_ID: + return relationship.spdx_element_id + elif relationship_property == RelationshipProperty.COMMENT: + return relationship.comment + elif relationship_property == RelationshipProperty.RELATED_SPDX_ELEMENT: + return str(relationship.related_spdx_element_id) + elif relationship_property == RelationshipProperty.RELATIONSHIP_TYPE: + return relationship.relationship_type.name + + def get_json_type(self) -> Type[JsonProperty]: + return RelationshipProperty + + def get_data_model_type(self) -> Type[Relationship]: + return Relationship diff --git a/src/jsonschema/relationship_properties.py b/src/jsonschema/relationship_properties.py new file mode 100644 index 000000000..bd6b787d6 --- /dev/null +++ b/src/jsonschema/relationship_properties.py @@ -0,0 +1,20 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import auto + +from src.jsonschema.json_property import JsonProperty + + +class RelationshipProperty(JsonProperty): + SPDX_ELEMENT_ID = auto() + COMMENT = auto() + RELATED_SPDX_ELEMENT = auto() + RELATIONSHIP_TYPE = auto() diff --git a/src/jsonschema/snippet_converter.py b/src/jsonschema/snippet_converter.py new file mode 100644 index 000000000..9abb7992e --- /dev/null +++ b/src/jsonschema/snippet_converter.py @@ -0,0 +1,88 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Type, Any, Tuple, Dict + +from src.jsonschema.annotation_converter import AnnotationConverter +from src.jsonschema.converter import TypedConverter +from src.jsonschema.json_property import JsonProperty +from src.jsonschema.optional_utils import apply_if_present +from src.jsonschema.snippet_properties import SnippetProperty +from src.model.document import Document +from src.model.snippet import Snippet + + +class SnippetConverter(TypedConverter[Snippet]): + annotation_converter: AnnotationConverter + + def __init__(self): + self.annotation_converter = AnnotationConverter() + + def json_property_name(self, snippet_property: SnippetProperty) -> str: + if snippet_property == SnippetProperty.SPDX_ID: + return "SPDXID" + return super().json_property_name(snippet_property) + + def _get_property_value(self, snippet: Snippet, snippet_property: SnippetProperty, + document: Document = None) -> Any: + if snippet_property == SnippetProperty.SPDX_ID: + return snippet.spdx_id + elif snippet_property == SnippetProperty.ANNOTATIONS: + snippet_annotations = filter(lambda annotation: annotation.spdx_id == snippet.spdx_id, document.annotations) + return [self.annotation_converter.convert(annotation) for annotation in snippet_annotations] or None + elif snippet_property == SnippetProperty.ATTRIBUTION_TEXTS: + return snippet.attribution_texts or None + elif snippet_property == SnippetProperty.COMMENT: + return snippet.comment + elif snippet_property == SnippetProperty.COPYRIGHT_TEXT: + return apply_if_present(str, snippet.copyright_text) + elif snippet_property == SnippetProperty.LICENSE_COMMENTS: + return snippet.license_comment + elif snippet_property == SnippetProperty.LICENSE_CONCLUDED: + return apply_if_present(str, snippet.concluded_license) + elif snippet_property == SnippetProperty.LICENSE_INFO_IN_SNIPPETS: + if isinstance(snippet.license_info_in_snippet, list): + return [str(license_expression) for license_expression in snippet.license_info_in_snippet] or None + return apply_if_present(str, snippet.license_info_in_snippet) + elif snippet_property == SnippetProperty.NAME: + return snippet.name + elif snippet_property == SnippetProperty.RANGES: + ranges = [convert_byte_range_to_dict(snippet.byte_range, snippet.file_spdx_id)] + if snippet.line_range: + ranges.append(convert_line_range_to_dict(snippet.line_range, snippet.file_spdx_id)) + return ranges + elif snippet_property == SnippetProperty.SNIPPET_FROM_FILE: + return snippet.file_spdx_id + + def get_json_type(self) -> Type[JsonProperty]: + return SnippetProperty + + def get_data_model_type(self) -> Type[Snippet]: + return Snippet + + def requires_full_document(self) -> bool: + return True + + +def convert_line_range_to_dict(line_range: Tuple[int, int], file_id: str) -> Dict: + return _convert_range_to_dict(line_range, file_id, "lineNumber") + + +def convert_byte_range_to_dict(byte_range: Tuple[int, int], file_id: str) -> Dict: + return _convert_range_to_dict(byte_range, file_id, "offset") + + +def _convert_range_to_dict(int_range: Tuple[int, int], file_id: str, pointer_property: str) -> Dict: + return {"startPointer": _pointer(file_id, int_range[0], pointer_property), + "endPointer": _pointer(file_id, int_range[1], pointer_property)} + + +def _pointer(reference: str, target: int, pointer_property: str) -> Dict: + return {"reference": reference, pointer_property: target} diff --git a/src/jsonschema/snippet_properties.py b/src/jsonschema/snippet_properties.py new file mode 100644 index 000000000..679326b26 --- /dev/null +++ b/src/jsonschema/snippet_properties.py @@ -0,0 +1,27 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import auto + +from src.jsonschema.json_property import JsonProperty + + +class SnippetProperty(JsonProperty): + SPDX_ID = auto() + ANNOTATIONS = auto() + ATTRIBUTION_TEXTS = auto() + COMMENT = auto() + COPYRIGHT_TEXT = auto() + LICENSE_COMMENTS = auto() + LICENSE_CONCLUDED = auto() + LICENSE_INFO_IN_SNIPPETS = auto() + NAME = auto() + RANGES = auto() + SNIPPET_FROM_FILE = auto() diff --git a/src/model/license_expression.py b/src/model/license_expression.py index 1f73ffc33..226b5e438 100644 --- a/src/model/license_expression.py +++ b/src/model/license_expression.py @@ -21,3 +21,6 @@ class LicenseExpression: def __init__(self, expression_string: str): check_types_and_set_values(self, locals()) + + def __str__(self): + return self.expression_string diff --git a/src/model/relationship_filters.py b/src/model/relationship_filters.py new file mode 100644 index 000000000..4d388b5f5 --- /dev/null +++ b/src/model/relationship_filters.py @@ -0,0 +1,43 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import List + +from src.model.document import Document +from src.model.package import Package +from src.model.relationship import Relationship, RelationshipType + + +def find_package_contains_file_relationships(document: Document, package: Package) -> List[Relationship]: + file_ids_in_document = [file.spdx_id for file in document.files] + package_contains_relationships = filter_by_type_and_origin(document.relationships, RelationshipType.CONTAINS, + package.spdx_id) + return [relationship for relationship in package_contains_relationships if + relationship.related_spdx_element_id in file_ids_in_document] + + +def find_file_contained_by_package_relationships(document: Document, package: Package) -> List[Relationship]: + file_ids_in_document = [file.spdx_id for file in document.files] + contained_by_package_relationships = filter_by_type_and_target(document.relationships, + RelationshipType.CONTAINED_BY, package.spdx_id) + return [relationship for relationship in contained_by_package_relationships if + relationship.spdx_element_id in file_ids_in_document] + + +def filter_by_type_and_target(relationships: List[Relationship], relationship_type: RelationshipType, + target_id: str) -> List[Relationship]: + return [relationship for relationship in relationships if + relationship.relationship_type == relationship_type and relationship.related_spdx_element_id == target_id] + + +def filter_by_type_and_origin(relationships: List[Relationship], relationship_type: RelationshipType, + origin_id: str) -> List[Relationship]: + return [relationship for relationship in relationships if + relationship.relationship_type == relationship_type and relationship.spdx_element_id == origin_id] diff --git a/src/model/snippet.py b/src/model/snippet.py index 329de5c25..690647fad 100644 --- a/src/model/snippet.py +++ b/src/model/snippet.py @@ -27,7 +27,7 @@ class Snippet: concluded_license: Optional[Union[LicenseExpression, SpdxNoAssertion, SpdxNone]] = None license_info_in_snippet: Optional[Union[List[LicenseExpression], SpdxNoAssertion, SpdxNone]] = None license_comment: Optional[str] = None - copyright_text: Optional[str] = None + copyright_text: Optional[Union[str, SpdxNoAssertion, SpdxNone]] = None comment: Optional[str] = None name: Optional[str] = None attribution_texts: List[str] = field(default_factory=list) diff --git a/src/model/spdx_no_assertion.py b/src/model/spdx_no_assertion.py index 60bec6fd6..3a12ed36c 100644 --- a/src/model/spdx_no_assertion.py +++ b/src/model/spdx_no_assertion.py @@ -9,19 +9,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +SPDX_NO_ASSERTION_STRING = "NOASSERTION" + class SpdxNoAssertion: """ Represents the SPDX NOASSERTION value. """ - _string_value: str = "NOASSERTION" - def __str__(self): - return self._string_value + return SPDX_NO_ASSERTION_STRING def __repr__(self): - return self._string_value + return SPDX_NO_ASSERTION_STRING def __eq__(self, other): return isinstance(other, SpdxNoAssertion) diff --git a/src/model/spdx_none.py b/src/model/spdx_none.py index 8e170ae4c..e4e97c534 100644 --- a/src/model/spdx_none.py +++ b/src/model/spdx_none.py @@ -9,19 +9,19 @@ # See the License for the specific language governing permissions and # limitations under the License. +SPDX_NONE_STRING = "NONE" + class SpdxNone: """ Represents the SPDX NONE value. """ - _string_value = "NONE" - def __str__(self): - return self._string_value + return SPDX_NONE_STRING def __repr__(self): - return self._string_value + return SPDX_NONE_STRING def __eq__(self, other): return isinstance(other, SpdxNone) diff --git a/src/validation/document_validator.py b/src/validation/document_validator.py index a0a151e21..bf2c11450 100644 --- a/src/validation/document_validator.py +++ b/src/validation/document_validator.py @@ -13,6 +13,7 @@ from src.model.document import Document from src.model.relationship import RelationshipType +from src.model.relationship_filters import filter_by_type_and_origin, filter_by_type_and_target from src.validation.annotation_validator import validate_annotations from src.validation.creation_info_validator import validate_creation_info from src.validation.extracted_licensing_info_validator import validate_extracted_licensing_infos @@ -35,15 +36,16 @@ def validate_full_spdx_document(document: Document, spdx_version: str) -> List[V validation_messages.extend(validate_extracted_licensing_infos(document.extracted_licensing_info)) document_id = document.creation_info.spdx_id - document_describes_relationships = [relationship for relationship in document.relationships if - relationship.relationship_type == RelationshipType.DESCRIBES and relationship.spdx_element_id == document_id] - described_by_document_relationships = [relationship for relationship in document.relationships if - relationship.relationship_type == RelationshipType.DESCRIBED_BY and relationship.related_spdx_element_id == document_id] + document_describes_relationships = filter_by_type_and_origin(document.relationships, RelationshipType.DESCRIBES, + document_id) + described_by_document_relationships = filter_by_type_and_target(document.relationships, + RelationshipType.DESCRIBED_BY, document_id) if not document_describes_relationships + described_by_document_relationships: validation_messages.append( ValidationMessage( - f'there must be at least one relationship "{document_id} DESCRIBES ..." or "... DESCRIBED_BY {document_id}"', + f'there must be at least one relationship "{document_id} DESCRIBES ..." or "... DESCRIBED_BY ' + f'{document_id}"', ValidationContext(spdx_id=document_id, element_type=SpdxElementType.DOCUMENT))) diff --git a/src/validation/spdx_id_validators.py b/src/validation/spdx_id_validators.py index 30e90be78..77bc91025 100644 --- a/src/validation/spdx_id_validators.py +++ b/src/validation/spdx_id_validators.py @@ -12,6 +12,7 @@ import re from typing import List +from src.document_utils import get_contained_spdx_element_ids from src.model.document import Document from src.model.file import File @@ -36,10 +37,7 @@ def is_spdx_id_present_in_document(spdx_id: str, document: Document) -> bool: def get_list_of_all_spdx_ids(document: Document) -> List[str]: all_spdx_ids_in_document: List[str] = [document.creation_info.spdx_id] - - all_spdx_ids_in_document.extend([package.spdx_id for package in document.packages]) - all_spdx_ids_in_document.extend([file.spdx_id for file in document.files]) - all_spdx_ids_in_document.extend([snippet.spdx_id for snippet in document.snippets]) + all_spdx_ids_in_document.extend(get_contained_spdx_element_ids(document)) return all_spdx_ids_in_document diff --git a/src/writer/__init__.py b/src/writer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/writer/casing_tools.py b/src/writer/casing_tools.py new file mode 100644 index 000000000..b14543093 --- /dev/null +++ b/src/writer/casing_tools.py @@ -0,0 +1,16 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from re import sub + + +def snake_case_to_camel_case(snake_case_string: str) -> str: + each_word_capitalized = sub(r"[_\-]+", " ", snake_case_string).title().replace(" ", "") + return each_word_capitalized[0].lower() + each_word_capitalized[1:] diff --git a/src/writer/json/__init__.py b/src/writer/json/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/writer/json/json_writer.py b/src/writer/json/json_writer.py new file mode 100644 index 000000000..5083c8caa --- /dev/null +++ b/src/writer/json/json_writer.py @@ -0,0 +1,35 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +from typing import List + +from src.jsonschema.document_converter import DocumentConverter +from src.model.document import Document +from src.validation.document_validator import validate_full_spdx_document +from src.validation.validation_message import ValidationMessage + + +def write_document(document: Document, file_name: str, validate: bool = True, converter: DocumentConverter = None): + """ + Serializes the provided document to json and writes it to a file with the provided name. Unless validate is set + to False, validates the document before serialization. Unless a DocumentConverter instance is provided, + a new one is created. + """ + if validate: + validation_messages: List[ValidationMessage] = validate_full_spdx_document(document, + document.creation_info.spdx_version) + if validation_messages: + raise ValueError(f"Document is not valid. The following errors were detected: {validation_messages}") + if converter is None: + converter = DocumentConverter() + document_dict = converter.convert(document) + with open(file_name, "w") as out: + json.dump(document_dict, out, indent=4) diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 000000000..12da10c32 --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,145 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from datetime import datetime + +from src.model.actor import Actor, ActorType +from src.model.annotation import Annotation, AnnotationType +from src.model.checksum import Checksum, ChecksumAlgorithm +from src.model.document import CreationInfo, Document +from src.model.external_document_ref import ExternalDocumentRef +from src.model.extracted_licensing_info import ExtractedLicensingInfo +from src.model.file import File, FileType +from src.model.license_expression import LicenseExpression +from src.model.package import Package, PackageVerificationCode, PackagePurpose, ExternalPackageRef, \ + ExternalPackageRefCategory +from src.model.relationship import Relationship, RelationshipType +from src.model.snippet import Snippet +from src.model.version import Version + +"""Utility methods to create data model instances. All properties have valid defaults, so they don't need to be +specified unless relevant for the test.""" + + +def creation_info_fixture(spdx_version="spdxVersion", spdx_id="documentId", name="documentName", + namespace="documentNamespace", creators=None, created=datetime(2022, 12, 1), + creator_comment="creatorComment", data_license="CC0-1.0", external_document_refs=None, + license_list_version=Version(3, 19), document_comment="documentComment") -> CreationInfo: + creators = [Actor(ActorType.PERSON, "creatorName")] if creators is None else creators + external_document_refs = [ + external_document_ref_fixture()] if external_document_refs is None else external_document_refs + return CreationInfo(spdx_version, spdx_id, name, namespace, creators, created, creator_comment, data_license, + external_document_refs, license_list_version, document_comment) + + +def file_fixture(name="fileName", spdx_id="fileId", checksums=None, file_type=None, + concluded_license=LicenseExpression("concludedLicenseExpression"), license_info_in_file=None, + license_comment="licenseComment", copyright_text="copyrightText", comment="fileComment", + notice="fileNotice", contributors=None, attribution_texts=None) -> File: + checksums = [Checksum(ChecksumAlgorithm.SHA1, "sha1")] if checksums is None else checksums + file_type = [FileType.TEXT] if file_type is None else file_type + license_info_in_file = [ + LicenseExpression("licenseInfoInFileExpression")] if license_info_in_file is None else license_info_in_file + contributors = ["fileContributor"] if contributors is None else contributors + attribution_texts = ["fileAttributionText"] if attribution_texts is None else attribution_texts + return File(name=name, spdx_id=spdx_id, checksums=checksums, file_type=file_type, + concluded_license=concluded_license, license_info_in_file=license_info_in_file, + license_comment=license_comment, copyright_text=copyright_text, comment=comment, notice=notice, + contributors=contributors, attribution_texts=attribution_texts) + + +def package_fixture(spdx_id="packageId", name="packageName", download_location="downloadLocation", + version="packageVersion", file_name="packageFileName", + supplier=Actor(ActorType.PERSON, "supplierName"), + originator=Actor(ActorType.PERSON, "originatorName"), files_analyzed=True, + verification_code=PackageVerificationCode("verificationCode"), checksums=None, + homepage="packageHomepage", source_info="sourceInfo", + license_concluded=LicenseExpression("packageLicenseConcluded"), license_info_from_files=None, + license_declared=LicenseExpression("packageLicenseDeclared"), + license_comment="packageLicenseComment", copyright_text="packageCopyrightText", + summary="packageSummary", description="packageDescription", comment="packageComment", + external_references=None, attribution_texts=None, primary_package_purpose=PackagePurpose.SOURCE, + release_date=datetime(2022, 12, 1), built_date=datetime(2022, 12, 2), + valid_until_date=datetime(2022, 12, 3)) -> Package: + checksums = [Checksum(ChecksumAlgorithm.SHA1, "packageSha1")] if checksums is None else checksums + license_info_from_files = [ + LicenseExpression("licenseInfoFromFile")] if license_info_from_files is None else license_info_from_files + external_references = [external_package_ref_fixture()] if external_references is None else external_references + attribution_texts = ["packageAttributionText"] if attribution_texts is None else attribution_texts + return Package(spdx_id=spdx_id, name=name, download_location=download_location, version=version, + file_name=file_name, supplier=supplier, originator=originator, files_analyzed=files_analyzed, + verification_code=verification_code, checksums=checksums, homepage=homepage, source_info=source_info, + license_concluded=license_concluded, license_info_from_files=license_info_from_files, + license_declared=license_declared, license_comment=license_comment, copyright_text=copyright_text, + summary=summary, description=description, comment=comment, external_references=external_references, + attribution_texts=attribution_texts, primary_package_purpose=primary_package_purpose, + release_date=release_date, built_date=built_date, valid_until_date=valid_until_date) + + +def external_document_ref_fixture(document_ref_id="externalDocumentRefId", document_uri="externalDocumentUri", + checksum=Checksum(ChecksumAlgorithm.MD5, + "externalDocumentRefMd5")) -> ExternalDocumentRef: + return ExternalDocumentRef(document_ref_id=document_ref_id, document_uri=document_uri, checksum=checksum) + + +def external_package_ref_fixture(category=ExternalPackageRefCategory.PACKAGE_MANAGER, + reference_type="externalPackageRefType", + locator="externalPackageRefLocator", + comment="externalPackageRefComment") -> ExternalPackageRef: + return ExternalPackageRef(category=category, reference_type=reference_type, locator=locator, comment=comment) + + +def snippet_fixture(spdx_id="snippetId", file_spdx_id="snippetFromFileId", byte_range=(1, 2), + line_range=(3, 4), concluded_license=LicenseExpression("snippetLicenseConcluded"), + license_info_in_snippet=None, license_comment="snippetLicenseComment", + copyright_text="licenseCopyrightText", comment="snippetComment", name="snippetName", + attribution_texts=None) -> Snippet: + license_info_in_snippet = [ + LicenseExpression("licenseInfoInSnippet")] if license_info_in_snippet is None else license_info_in_snippet + attribution_texts = ["snippetAttributionText"] if attribution_texts is None else attribution_texts + return Snippet(spdx_id=spdx_id, file_spdx_id=file_spdx_id, byte_range=byte_range, line_range=line_range, + concluded_license=concluded_license, license_info_in_snippet=license_info_in_snippet, + license_comment=license_comment, copyright_text=copyright_text, comment=comment, name=name, + attribution_texts=attribution_texts) + + +def annotation_fixture(spdx_id="annotatedElementId", annotation_type=AnnotationType.REVIEW, + annotator=Actor(ActorType.PERSON, "annotatorName"), annotation_date=datetime(2022, 12, 1), + annotation_comment="annotationComment") -> Annotation: + return Annotation(spdx_id=spdx_id, annotation_type=annotation_type, annotator=annotator, + annotation_date=annotation_date, annotation_comment=annotation_comment) + + +def extracted_licensing_info_fixture(license_id="licenseId", extracted_text="extractedText", license_name="licenseName", + cross_references=None, comment="licenseComment") -> ExtractedLicensingInfo: + cross_references = ["crossReference"] if cross_references is None else cross_references + return ExtractedLicensingInfo(license_id=license_id, extracted_text=extracted_text, license_name=license_name, + cross_references=cross_references, comment=comment) + + +def document_fixture(creation_info=None, packages=None, files=None, snippets=None, annotations=None, relationships=None, + extracted_licensing_info=None) -> Document: + creation_info = creation_info_fixture() if creation_info is None else creation_info + packages = [package_fixture()] if packages is None else packages + files = [file_fixture()] if files is None else files + snippets = [snippet_fixture()] if snippets is None else snippets + annotations = [annotation_fixture()] if annotations is None else annotations + relationships = [relationship_fixture()] if relationships is None else relationships + extracted_licensing_info = [ + extracted_licensing_info_fixture()] if extracted_licensing_info is None else extracted_licensing_info + return Document(creation_info=creation_info, packages=packages, files=files, snippets=snippets, + annotations=annotations, relationships=relationships, + extracted_licensing_info=extracted_licensing_info) + + +def relationship_fixture(spdx_element_id="relationshipOriginId", relationship_type=RelationshipType.DESCRIBES, + related_spdx_element_id="relationshipTargetId", comment="relationshipComment") -> Relationship: + return Relationship(spdx_element_id=spdx_element_id, relationship_type=relationship_type, + related_spdx_element_id=related_spdx_element_id, comment=comment) diff --git a/tests/jsonschema/__init__.py b/tests/jsonschema/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/jsonschema/test_annotation_converter.py b/tests/jsonschema/test_annotation_converter.py new file mode 100644 index 000000000..714b7bb4a --- /dev/null +++ b/tests/jsonschema/test_annotation_converter.py @@ -0,0 +1,56 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from datetime import datetime + +import pytest + +from src.datetime_conversions import datetime_to_iso_string +from src.jsonschema.annotation_converter import AnnotationConverter +from src.jsonschema.annotation_properties import AnnotationProperty +from src.model.actor import Actor, ActorType +from src.model.annotation import Annotation, AnnotationType + + +@pytest.fixture +def converter() -> AnnotationConverter: + return AnnotationConverter() + + +@pytest.mark.parametrize("annotation_property,expected", [(AnnotationProperty.ANNOTATION_DATE, "annotationDate"), + (AnnotationProperty.ANNOTATION_TYPE, "annotationType"), + (AnnotationProperty.ANNOTATOR, "annotator"), + (AnnotationProperty.COMMENT, "comment")]) +def test_json_property_names(converter: AnnotationConverter, annotation_property: AnnotationProperty, expected: str): + assert converter.json_property_name(annotation_property) == expected + + +def test_json_type(converter: AnnotationConverter): + assert converter.get_json_type() == AnnotationProperty + + +def test_data_model_type(converter: AnnotationConverter): + assert converter.get_data_model_type() == Annotation + + +def test_successful_conversion(converter: AnnotationConverter): + date = datetime(2022, 12, 1) + annotator = Actor(ActorType.PERSON, "actorName") + annotation = Annotation("spdxId", AnnotationType.REVIEW, annotator, + date, "comment") + + converted_dict = converter.convert(annotation) + + assert converted_dict == { + converter.json_property_name(AnnotationProperty.ANNOTATION_DATE): datetime_to_iso_string(date), + converter.json_property_name(AnnotationProperty.ANNOTATION_TYPE): "REVIEW", + converter.json_property_name(AnnotationProperty.ANNOTATOR): annotator.to_serialized_string(), + converter.json_property_name(AnnotationProperty.COMMENT): "comment" + } diff --git a/tests/jsonschema/test_checksum_converter.py b/tests/jsonschema/test_checksum_converter.py new file mode 100644 index 000000000..9412761de --- /dev/null +++ b/tests/jsonschema/test_checksum_converter.py @@ -0,0 +1,45 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +from src.jsonschema.checksum_converter import ChecksumConverter +from src.jsonschema.checksum_properties import ChecksumProperty +from src.model.checksum import Checksum, ChecksumAlgorithm + + +@pytest.fixture +def converter() -> ChecksumConverter: + return ChecksumConverter() + + +@pytest.mark.parametrize("checksum_property,expected", [(ChecksumProperty.ALGORITHM, "algorithm"), + (ChecksumProperty.CHECKSUM_VALUE, "checksumValue")]) +def test_json_property_names(converter: ChecksumConverter, checksum_property: ChecksumProperty, expected: str): + assert converter.json_property_name(checksum_property) == expected + + +def test_successful_conversion(converter: ChecksumConverter): + checksum = Checksum(ChecksumAlgorithm.SHA1, "123") + + converted_dict = converter.convert(checksum) + + assert converted_dict == { + converter.json_property_name(ChecksumProperty.ALGORITHM): "SHA1", + converter.json_property_name(ChecksumProperty.CHECKSUM_VALUE): "123" + } + + +def test_json_type(converter: ChecksumConverter): + assert converter.get_json_type() == ChecksumProperty + + +def test_data_model_type(converter: ChecksumConverter): + assert converter.get_data_model_type() == Checksum diff --git a/tests/jsonschema/test_converter.py b/tests/jsonschema/test_converter.py new file mode 100644 index 000000000..0123c3b19 --- /dev/null +++ b/tests/jsonschema/test_converter.py @@ -0,0 +1,82 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import auto +from typing import Type, Any + +import pytest + +from src.jsonschema.converter import TypedConverter +from src.jsonschema.json_property import JsonProperty +from src.model.checksum import Checksum, ChecksumAlgorithm +from src.model.document import Document +from src.model.typing.dataclass_with_properties import dataclass_with_properties +from src.model.typing.type_checks import check_types_and_set_values + + +class TestPropertyType(JsonProperty): + FIRST_NAME = auto() + SECOND_NAME = auto() + + +@dataclass_with_properties +class TestDataModelType: + first_property: str + second_property: int + third_property: int + + def __init__(self, first_property: str, second_property: int, third_property: int): + check_types_and_set_values(self, locals()) + + +class TestConverter(TypedConverter): + def json_property_name(self, test_property: TestPropertyType) -> str: + if test_property == TestPropertyType.FIRST_NAME: + return "jsonFirstName" + else: + return "jsonSecondName" + + def _get_property_value(self, instance: TestDataModelType, test_property: TestPropertyType, + _document: Document = None) -> Any: + if test_property == TestPropertyType.FIRST_NAME: + return instance.first_property + elif test_property == TestPropertyType.SECOND_NAME: + return instance.second_property + instance.third_property + + def get_json_type(self) -> Type[JsonProperty]: + return TestPropertyType + + def get_data_model_type(self) -> Type: + return TestDataModelType + + +def test_conversion(): + converter = TestConverter() + test_instance = TestDataModelType("firstPropertyValue", 1, 2) + + converted_dict = converter.convert(test_instance) + + assert converted_dict == { + "jsonFirstName": "firstPropertyValue", + "jsonSecondName": 3 + } + + +def test_wrong_type(): + converter = TestConverter() + checksum = Checksum(ChecksumAlgorithm.SHA1, "123") + + with pytest.raises(TypeError) as error: + converter.convert(checksum) + + error_message: str = error.value.args[0] + assert TestConverter.__name__ in error_message + assert TestDataModelType.__name__ in error_message + assert Checksum.__name__ in error_message diff --git a/tests/jsonschema/test_creation_info_converter.py b/tests/jsonschema/test_creation_info_converter.py new file mode 100644 index 000000000..87cd0a0a1 --- /dev/null +++ b/tests/jsonschema/test_creation_info_converter.py @@ -0,0 +1,69 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from datetime import datetime + +import pytest + +from src.datetime_conversions import datetime_to_iso_string +from src.jsonschema.creation_info_converter import CreationInfoConverter +from src.jsonschema.creation_info_properties import CreationInfoProperty +from src.model.actor import Actor, ActorType +from src.model.document import CreationInfo +from src.model.version import Version +from tests.fixtures import creation_info_fixture + + +@pytest.fixture +def converter() -> CreationInfoConverter: + return CreationInfoConverter() + + +@pytest.mark.parametrize("creation_info_property,expected", + [(CreationInfoProperty.CREATED, "created"), (CreationInfoProperty.CREATORS, "creators"), + (CreationInfoProperty.LICENSE_LIST_VERSION, "licenseListVersion"), + (CreationInfoProperty.COMMENT, "comment")]) +def test_json_property_names(converter: CreationInfoConverter, creation_info_property: CreationInfoProperty, + expected: str): + assert converter.json_property_name(creation_info_property) == expected + + +def test_successful_conversion(converter: CreationInfoConverter): + creators = [Actor(ActorType.PERSON, "personName"), Actor(ActorType.TOOL, "toolName")] + created = datetime(2022, 12, 1) + + converted_dict = converter.convert( + creation_info_fixture(creators=creators, created=created, creator_comment="comment", + license_list_version=Version(1, 2))) + + assert converted_dict == { + converter.json_property_name(CreationInfoProperty.CREATED): datetime_to_iso_string(created), + converter.json_property_name(CreationInfoProperty.CREATORS): ["Person: personName", "Tool: toolName"], + converter.json_property_name(CreationInfoProperty.LICENSE_LIST_VERSION): "1.2", + converter.json_property_name(CreationInfoProperty.COMMENT): "comment" + } + + +def test_null_values(converter: CreationInfoConverter): + creation_info = creation_info_fixture(license_list_version=None, creator_comment=None, creators=[]) + + converted_dict = converter.convert(creation_info) + + assert converter.json_property_name(CreationInfoProperty.LICENSE_LIST_VERSION) not in converted_dict + assert converter.json_property_name(CreationInfoProperty.COMMENT) not in converted_dict + assert converter.json_property_name(CreationInfoProperty.CREATORS) not in converted_dict + + +def test_json_type(converter: CreationInfoConverter): + assert converter.get_json_type() == CreationInfoProperty + + +def test_data_model_type(converter: CreationInfoConverter): + assert converter.get_data_model_type() == CreationInfo diff --git a/tests/jsonschema/test_document_converter.py b/tests/jsonschema/test_document_converter.py new file mode 100644 index 000000000..a39b1c64b --- /dev/null +++ b/tests/jsonschema/test_document_converter.py @@ -0,0 +1,226 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from datetime import datetime +from typing import Union +from unittest import mock +from unittest.mock import MagicMock, NonCallableMagicMock + +import pytest + +from src.jsonschema.annotation_converter import AnnotationConverter +from src.jsonschema.document_converter import DocumentConverter +from src.jsonschema.document_properties import DocumentProperty +from src.jsonschema.relationship_converter import RelationshipConverter +from src.model.actor import Actor, ActorType +from src.model.annotation import Annotation, AnnotationType +from src.model.document import Document +from src.model.extracted_licensing_info import ExtractedLicensingInfo +from src.model.relationship import Relationship, RelationshipType +from tests.fixtures import creation_info_fixture, file_fixture, package_fixture, external_document_ref_fixture, \ + snippet_fixture, annotation_fixture, document_fixture, relationship_fixture +from tests.mock_utils import assert_mock_method_called_with_arguments, assert_no_mock_methods_called + + +@pytest.fixture +@mock.patch('src.jsonschema.creation_info_converter.CreationInfoConverter', autospec=True) +@mock.patch('src.jsonschema.external_document_ref_converter.ExternalDocumentRefConverter', autospec=True) +@mock.patch('src.jsonschema.package_converter.PackageConverter', autospec=True) +@mock.patch('src.jsonschema.annotation_converter.AnnotationConverter', autospec=True) +@mock.patch('src.jsonschema.extracted_licensing_info_converter.ExtractedLicensingInfoConverter', autospec=True) +@mock.patch('src.jsonschema.file_converter.FileConverter', autospec=True) +@mock.patch('src.jsonschema.snippet_converter.SnippetConverter', autospec=True) +@mock.patch('src.jsonschema.relationship_converter.RelationshipConverter', autospec=True) +def converter(relationship_converter_mock: MagicMock, snippet_converter_mock: MagicMock, file_converter_mock: MagicMock, + extracted_licensing_info_converter_mock: MagicMock, annotation_converter_mock: MagicMock, + package_converter_mock: MagicMock, external_ref_converter_mock: MagicMock, + creation_info_converter_mock: MagicMock) -> DocumentConverter: + converter = DocumentConverter() + converter.creation_info_converter = creation_info_converter_mock() + converter.external_document_ref_converter = external_ref_converter_mock() + converter.package_converter = package_converter_mock() + converter.annotation_converter = annotation_converter_mock() + converter.extracted_licensing_info_converter = extracted_licensing_info_converter_mock() + converter.file_converter = file_converter_mock() + converter.snippet_converter = snippet_converter_mock() + converter.relationship_converter = relationship_converter_mock() + return converter + + +@pytest.mark.parametrize("document_property,expected", + [(DocumentProperty.SPDX_VERSION, "spdxVersion"), (DocumentProperty.SPDX_ID, "SPDXID"), + (DocumentProperty.NAME, "name"), (DocumentProperty.DOCUMENT_NAMESPACE, "documentNamespace"), + (DocumentProperty.DATA_LICENSE, "dataLicense"), + (DocumentProperty.EXTERNAL_DOCUMENT_REFS, "externalDocumentRefs"), + (DocumentProperty.COMMENT, "comment"), (DocumentProperty.CREATION_INFO, "creationInfo"), + (DocumentProperty.PACKAGES, "packages"), (DocumentProperty.FILES, "files"), + (DocumentProperty.SNIPPETS, "snippets"), (DocumentProperty.ANNOTATIONS, "annotations"), + (DocumentProperty.RELATIONSHIPS, "relationships"), + (DocumentProperty.HAS_EXTRACTED_LICENSING_INFO, "hasExtractedLicensingInfo")]) +def test_json_property_names(converter: DocumentConverter, document_property: DocumentProperty, + expected: str): + assert converter.json_property_name(document_property) == expected + + +def test_successful_conversion(converter: DocumentConverter): + creation_info = creation_info_fixture(spdx_version="spdxVersion", spdx_id="spdxId", name="name", + namespace="namespace", document_comment="comment", data_license="dataLicense", + external_document_refs=[external_document_ref_fixture()]) + document = Document(creation_info, annotations=[ + Annotation("annotationId", AnnotationType.REVIEW, Actor(ActorType.PERSON, "reviewerName"), + datetime(2022, 12, 1), "reviewComment")], + extracted_licensing_info=[ExtractedLicensingInfo("licenseId", "licenseText")], relationships=[ + Relationship(creation_info.spdx_id, RelationshipType.DESCRIBES, "describedElementId"), + Relationship("relationshipOriginId", RelationshipType.AMENDS, "relationShipTargetId")], + packages=[package_fixture()], files=[file_fixture()], snippets=[snippet_fixture()]) + converter.external_document_ref_converter.convert.return_value = "mock_converted_external_ref" + converter.creation_info_converter.convert.return_value = "mock_converted_creation_info" + converter.package_converter.convert.return_value = "mock_converted_package" + converter.annotation_converter.convert.return_value = "mock_converted_annotation" + converter.extracted_licensing_info_converter.convert.return_value = "mock_converted_extracted_licensing_info" + converter.package_converter.convert.return_value = "mock_converted_package" + converter.file_converter.convert.return_value = "mock_converted_file" + converter.snippet_converter.convert.return_value = "mock_converted_snippet" + converter.relationship_converter.convert.return_value = "mock_converted_relationship" + + converted_dict = converter.convert(document) + + assert converted_dict == { + converter.json_property_name(DocumentProperty.SPDX_ID): "spdxId", + converter.json_property_name(DocumentProperty.ANNOTATIONS): ["mock_converted_annotation"], + converter.json_property_name(DocumentProperty.COMMENT): "comment", + converter.json_property_name(DocumentProperty.CREATION_INFO): "mock_converted_creation_info", + converter.json_property_name(DocumentProperty.DATA_LICENSE): "dataLicense", + converter.json_property_name(DocumentProperty.EXTERNAL_DOCUMENT_REFS): ["mock_converted_external_ref"], + converter.json_property_name(DocumentProperty.HAS_EXTRACTED_LICENSING_INFO): [ + "mock_converted_extracted_licensing_info"], + converter.json_property_name(DocumentProperty.NAME): "name", + converter.json_property_name(DocumentProperty.SPDX_VERSION): "spdxVersion", + converter.json_property_name(DocumentProperty.DOCUMENT_NAMESPACE): "namespace", + converter.json_property_name(DocumentProperty.DOCUMENT_DESCRIBES): ["describedElementId"], + converter.json_property_name(DocumentProperty.PACKAGES): ["mock_converted_package"], + converter.json_property_name(DocumentProperty.FILES): ["mock_converted_file"], + converter.json_property_name(DocumentProperty.SNIPPETS): ["mock_converted_snippet"], + converter.json_property_name(DocumentProperty.RELATIONSHIPS): ["mock_converted_relationship"] + } + + +def test_json_type(converter: DocumentConverter): + assert converter.get_json_type() == DocumentProperty + + +def test_data_model_type(converter: DocumentConverter): + assert converter.get_data_model_type() == Document + + +def test_null_values(converter: DocumentConverter): + document = Document(creation_info_fixture(external_document_refs=[])) + + converted_dict = converter.convert(document) + + assert converter.json_property_name(DocumentProperty.ANNOTATIONS) not in converted_dict + assert converter.json_property_name(DocumentProperty.EXTERNAL_DOCUMENT_REFS) not in converted_dict + assert converter.json_property_name(DocumentProperty.HAS_EXTRACTED_LICENSING_INFO) not in converted_dict + assert converter.json_property_name(DocumentProperty.DOCUMENT_DESCRIBES) not in converted_dict + assert converter.json_property_name(DocumentProperty.PACKAGES) not in converted_dict + assert converter.json_property_name(DocumentProperty.FILES) not in converted_dict + assert converter.json_property_name(DocumentProperty.SNIPPETS) not in converted_dict + assert converter.json_property_name(DocumentProperty.RELATIONSHIPS) not in converted_dict + + +def test_document_annotations(converter: DocumentConverter): + file = file_fixture(spdx_id="fileId") + package = package_fixture(spdx_id="packageId") + snippet = snippet_fixture(spdx_id="snippetId") + document_id = "documentId" + + # There are 5 annotations: one each referencing the document, package, file and snippet, and one with an id + # matching none of the Spdx elements. The writer is expected to add the package, file and snippet annotations to + # those elements, so the document should receive the other two. + document_annotation = annotation_fixture(spdx_id=document_id) + other_annotation = annotation_fixture(spdx_id="otherId") + annotations = [annotation_fixture(spdx_id=file.spdx_id), annotation_fixture(spdx_id=package.spdx_id), + annotation_fixture(spdx_id=snippet.spdx_id), document_annotation, + other_annotation] + document = Document(creation_info_fixture(spdx_id=document_id), files=[file], packages=[package], + snippets=[snippet], annotations=annotations) + + # Weird type hint to make warnings about unresolved references from the mock class disappear + annotation_converter: Union[AnnotationConverter, NonCallableMagicMock] = converter.annotation_converter + annotation_converter.convert.return_value = "mock_converted_annotation" + + converted_dict = converter.convert(document) + + assert_mock_method_called_with_arguments(annotation_converter, "convert", document_annotation, other_annotation) + converted_document_annotations = converted_dict.get(converter.json_property_name(DocumentProperty.ANNOTATIONS)) + assert converted_document_annotations == ["mock_converted_annotation", "mock_converted_annotation"] + + +def test_document_describes(converter: DocumentConverter): + document = document_fixture() + document_id = document.creation_info.spdx_id + document_describes_relationship = relationship_fixture(spdx_element_id=document_id, + relationship_type=RelationshipType.DESCRIBES, + related_spdx_element_id="describesId") + described_by_document_relationship = relationship_fixture(related_spdx_element_id=document_id, + relationship_type=RelationshipType.DESCRIBED_BY, + spdx_element_id="describedById") + other_describes_relationship = relationship_fixture(relationship_type=RelationshipType.DESCRIBES) + other_relationship = relationship_fixture(spdx_element_id=document_id, relationship_type=RelationshipType.CONTAINS) + document.relationships = [document_describes_relationship, described_by_document_relationship, + other_describes_relationship, other_relationship] + + converted_dict = converter.convert(document) + + document_describes = converted_dict.get(converter.json_property_name(DocumentProperty.DOCUMENT_DESCRIBES)) + assert document_describes == [document_describes_relationship.related_spdx_element_id, + described_by_document_relationship.spdx_element_id] + + +DOCUMENT_ID = "docConverterTestDocumentId" +PACKAGE_ID = "docConverterTestPackageId" +FILE_ID = "docConverterTestFileId" + + +@pytest.mark.parametrize("relationship,should_be_written", + [(relationship_fixture(DOCUMENT_ID, RelationshipType.DESCRIBES), True), + (relationship_fixture(DOCUMENT_ID, RelationshipType.DESCRIBES, comment=None), False), + (relationship_fixture(relationship_type=RelationshipType.DESCRIBED_BY, + related_spdx_element_id=DOCUMENT_ID), True), + (relationship_fixture(relationship_type=RelationshipType.DESCRIBED_BY, + related_spdx_element_id=DOCUMENT_ID, comment=None), False), + (relationship_fixture(DOCUMENT_ID, RelationshipType.AMENDS, comment=None), True), + (relationship_fixture(PACKAGE_ID, RelationshipType.CONTAINS, FILE_ID), True), + (relationship_fixture(PACKAGE_ID, RelationshipType.CONTAINS, FILE_ID, comment=None), False), + (relationship_fixture(FILE_ID, RelationshipType.CONTAINED_BY, PACKAGE_ID), True), + (relationship_fixture(FILE_ID, RelationshipType.CONTAINED_BY, PACKAGE_ID, comment=None), + False), + (relationship_fixture(PACKAGE_ID, RelationshipType.CONTAINS, comment=None), True), + (relationship_fixture(PACKAGE_ID, RelationshipType.COPY_OF, FILE_ID, comment=None), True)]) +def test_document_relationships(converter: DocumentConverter, relationship: Relationship, should_be_written: bool): + package = package_fixture(spdx_id=PACKAGE_ID) + file = file_fixture(spdx_id=FILE_ID) + document = document_fixture(creation_info_fixture(spdx_id=DOCUMENT_ID), packages=[package], files=[file], + relationships=[relationship]) + + # Weird type hint to make warnings about unresolved references from the mock class disappear + relationship_converter: Union[RelationshipConverter, NonCallableMagicMock] = converter.relationship_converter + relationship_converter.convert.return_value = "mock_converted_relationship" + + converted_dict = converter.convert(document) + + relationships = converted_dict.get(converter.json_property_name(DocumentProperty.RELATIONSHIPS)) + + if should_be_written: + assert_mock_method_called_with_arguments(relationship_converter, "convert", relationship) + assert relationships == ["mock_converted_relationship"] + else: + assert_no_mock_methods_called(relationship_converter) + assert relationships is None diff --git a/tests/jsonschema/test_external_document_ref_converter.py b/tests/jsonschema/test_external_document_ref_converter.py new file mode 100644 index 000000000..51932c125 --- /dev/null +++ b/tests/jsonschema/test_external_document_ref_converter.py @@ -0,0 +1,59 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from unittest import mock +from unittest.mock import MagicMock + +import pytest + +from src.jsonschema.external_document_ref_converter import ExternalDocumentRefConverter +from src.jsonschema.external_document_ref_properties import ExternalDocumentRefProperty +from src.model.checksum import Checksum, ChecksumAlgorithm +from src.model.external_document_ref import ExternalDocumentRef + + +@pytest.fixture +@mock.patch('src.jsonschema.checksum_converter.ChecksumConverter', autospec=True) +def converter(checksum_converter_magic_mock: MagicMock) -> ExternalDocumentRefConverter: + mocked_checksum_converter = checksum_converter_magic_mock() + converter = ExternalDocumentRefConverter() + converter.checksum_converter = mocked_checksum_converter + return converter + + +@pytest.mark.parametrize("external_document_ref_property,expected", + [(ExternalDocumentRefProperty.EXTERNAL_DOCUMENT_ID, "externalDocumentId"), + (ExternalDocumentRefProperty.SPDX_DOCUMENT, "spdxDocument"), + (ExternalDocumentRefProperty.CHECKSUM, "checksum")]) +def test_json_property_names(converter: ExternalDocumentRefConverter, + external_document_ref_property: ExternalDocumentRefProperty, expected: str): + assert converter.json_property_name(external_document_ref_property) == expected + + +def test_successful_conversion(converter: ExternalDocumentRefConverter): + converter.checksum_converter.convert.return_value = "mock_converted_checksum" + checksum = Checksum(ChecksumAlgorithm.SHA1, "123") + external_document_ref = ExternalDocumentRef("document_ref_id", "document_uri", checksum) + + converted_dict = converter.convert(external_document_ref) + + assert converted_dict == { + converter.json_property_name(ExternalDocumentRefProperty.EXTERNAL_DOCUMENT_ID): "document_ref_id", + converter.json_property_name(ExternalDocumentRefProperty.SPDX_DOCUMENT): "document_uri", + converter.json_property_name(ExternalDocumentRefProperty.CHECKSUM): "mock_converted_checksum" + } + + +def test_json_type(converter: ExternalDocumentRefConverter): + assert converter.get_json_type() == ExternalDocumentRefProperty + + +def test_data_model_type(converter: ExternalDocumentRefConverter): + assert converter.get_data_model_type() == ExternalDocumentRef diff --git a/tests/jsonschema/test_external_package_ref_converter.py b/tests/jsonschema/test_external_package_ref_converter.py new file mode 100644 index 000000000..7af0752e1 --- /dev/null +++ b/tests/jsonschema/test_external_package_ref_converter.py @@ -0,0 +1,51 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +from src.jsonschema.external_package_ref_converter import ExternalPackageRefConverter +from src.jsonschema.external_package_ref_properties import ExternalPackageRefProperty +from src.model.package import ExternalPackageRef, ExternalPackageRefCategory + + +@pytest.fixture +def converter() -> ExternalPackageRefConverter: + return ExternalPackageRefConverter() + + +@pytest.mark.parametrize("external_package_ref_property,expected", + [(ExternalPackageRefProperty.COMMENT, "comment"), + (ExternalPackageRefProperty.REFERENCE_CATEGORY, "referenceCategory"), + (ExternalPackageRefProperty.REFERENCE_LOCATOR, "referenceLocator"), + (ExternalPackageRefProperty.REFERENCE_TYPE, "referenceType")]) +def test_json_property_names(converter: ExternalPackageRefConverter, + external_package_ref_property: ExternalPackageRefProperty, expected: str): + assert converter.json_property_name(external_package_ref_property) == expected + + +def test_json_type(converter: ExternalPackageRefConverter): + assert converter.get_json_type() == ExternalPackageRefProperty + + +def test_data_model_type(converter: ExternalPackageRefConverter): + assert converter.get_data_model_type() == ExternalPackageRef + + +def test_successful_conversion(converter: ExternalPackageRefConverter): + external_package_ref = ExternalPackageRef(ExternalPackageRefCategory.PACKAGE_MANAGER, "type", "locator", "comment") + + converted_dict = converter.convert(external_package_ref) + + assert converted_dict == { + converter.json_property_name(ExternalPackageRefProperty.COMMENT): "comment", + converter.json_property_name(ExternalPackageRefProperty.REFERENCE_CATEGORY): "PACKAGE_MANAGER", + converter.json_property_name(ExternalPackageRefProperty.REFERENCE_LOCATOR): "locator", + converter.json_property_name(ExternalPackageRefProperty.REFERENCE_TYPE): "type" + } diff --git a/tests/jsonschema/test_extracted_licensing_info_converter.py b/tests/jsonschema/test_extracted_licensing_info_converter.py new file mode 100644 index 000000000..c90d5c311 --- /dev/null +++ b/tests/jsonschema/test_extracted_licensing_info_converter.py @@ -0,0 +1,77 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +from src.jsonschema.extracted_licensing_info_converter import ExtractedLicensingInfoConverter +from src.jsonschema.extracted_licensing_info_properties import ExtractedLicensingInfoProperty +from src.model.extracted_licensing_info import ExtractedLicensingInfo +from src.model.spdx_no_assertion import SpdxNoAssertion, SPDX_NO_ASSERTION_STRING +from tests.fixtures import extracted_licensing_info_fixture + + +@pytest.fixture +def converter() -> ExtractedLicensingInfoConverter: + return ExtractedLicensingInfoConverter() + + +@pytest.mark.parametrize("extracted_licensing_info_property,expected", + [(ExtractedLicensingInfoProperty.LICENSE_ID, "licenseId"), + (ExtractedLicensingInfoProperty.EXTRACTED_TEXT, "extractedText"), + (ExtractedLicensingInfoProperty.NAME, "name"), + (ExtractedLicensingInfoProperty.COMMENT, "comment"), + (ExtractedLicensingInfoProperty.SEE_ALSOS, "seeAlsos")]) +def test_json_property_names(converter: ExtractedLicensingInfoConverter, + extracted_licensing_info_property: ExtractedLicensingInfoProperty, expected: str): + assert converter.json_property_name(extracted_licensing_info_property) == expected + + +def test_json_type(converter: ExtractedLicensingInfoConverter): + assert converter.get_json_type() == ExtractedLicensingInfoProperty + + +def test_data_model_type(converter: ExtractedLicensingInfoConverter): + assert converter.get_data_model_type() == ExtractedLicensingInfo + + +def test_successful_conversion(converter: ExtractedLicensingInfoConverter): + extracted_licensing_info = ExtractedLicensingInfo(license_id="licenseId", extracted_text="Extracted text", + license_name="license name", + cross_references=["reference1", "reference2"], comment="comment") + + converted_dict = converter.convert(extracted_licensing_info) + + assert converted_dict == { + converter.json_property_name(ExtractedLicensingInfoProperty.LICENSE_ID): "licenseId", + converter.json_property_name(ExtractedLicensingInfoProperty.EXTRACTED_TEXT): "Extracted text", + converter.json_property_name(ExtractedLicensingInfoProperty.NAME): "license name", + converter.json_property_name(ExtractedLicensingInfoProperty.SEE_ALSOS): ["reference1", "reference2"], + converter.json_property_name(ExtractedLicensingInfoProperty.COMMENT): "comment" + } + + +def test_null_values(converter: ExtractedLicensingInfoConverter): + extracted_licensing_info = ExtractedLicensingInfo(cross_references=[]) + + converted_dict = converter.convert(extracted_licensing_info) + + assert converter.json_property_name(ExtractedLicensingInfoProperty.LICENSE_ID) not in converted_dict + assert converter.json_property_name(ExtractedLicensingInfoProperty.EXTRACTED_TEXT) not in converted_dict + assert converter.json_property_name(ExtractedLicensingInfoProperty.NAME) not in converted_dict + assert converter.json_property_name(ExtractedLicensingInfoProperty.SEE_ALSOS) not in converted_dict + assert converter.json_property_name(ExtractedLicensingInfoProperty.COMMENT) not in converted_dict + + +def test_spdx_no_assertion(converter: ExtractedLicensingInfoConverter): + extracted_licensing_info = extracted_licensing_info_fixture(license_name=SpdxNoAssertion()) + + converted_dict = converter.convert(extracted_licensing_info) + + assert converted_dict[converter.json_property_name(ExtractedLicensingInfoProperty.NAME)] == SPDX_NO_ASSERTION_STRING diff --git a/tests/jsonschema/test_file_converter.py b/tests/jsonschema/test_file_converter.py new file mode 100644 index 000000000..5d291b012 --- /dev/null +++ b/tests/jsonschema/test_file_converter.py @@ -0,0 +1,172 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from datetime import datetime +from typing import Union +from unittest import mock +from unittest.mock import MagicMock, NonCallableMagicMock + +import pytest + +from src.jsonschema.annotation_converter import AnnotationConverter +from src.jsonschema.file_converter import FileConverter +from src.jsonschema.file_properties import FileProperty +from src.model.actor import Actor, ActorType +from src.model.annotation import Annotation, AnnotationType +from src.model.checksum import Checksum, ChecksumAlgorithm +from src.model.document import Document +from src.model.file import File, FileType +from src.model.license_expression import LicenseExpression +from src.model.spdx_no_assertion import SpdxNoAssertion, SPDX_NO_ASSERTION_STRING +from src.model.spdx_none import SpdxNone, SPDX_NONE_STRING +from tests.fixtures import creation_info_fixture, file_fixture, annotation_fixture, document_fixture +from tests.mock_utils import assert_mock_method_called_with_arguments + + +@pytest.fixture +@mock.patch('src.jsonschema.checksum_converter.ChecksumConverter', autospec=True) +@mock.patch('src.jsonschema.annotation_converter.AnnotationConverter', autospec=True) +def converter(annotation_converter_mock: MagicMock, checksum_converter_mock: MagicMock) -> FileConverter: + converter = FileConverter() + converter.checksum_converter = checksum_converter_mock() + converter.annotation_converter = annotation_converter_mock() + return converter + + +@pytest.mark.parametrize("file_property,expected", + [(FileProperty.SPDX_ID, "SPDXID"), + (FileProperty.ANNOTATIONS, "annotations"), + (FileProperty.ARTIFACT_OFS, "artifactOfs"), + (FileProperty.ATTRIBUTION_TEXTS, "attributionTexts"), + (FileProperty.CHECKSUMS, "checksums"), + (FileProperty.COMMENT, "comment"), + (FileProperty.COPYRIGHT_TEXT, "copyrightText"), + (FileProperty.FILE_CONTRIBUTORS, "fileContributors"), + (FileProperty.FILE_DEPENDENCIES, "fileDependencies"), + (FileProperty.FILE_NAME, "fileName"), + (FileProperty.FILE_TYPES, "fileTypes"), + (FileProperty.LICENSE_COMMENTS, "licenseComments"), + (FileProperty.LICENSE_CONCLUDED, "licenseConcluded"), + (FileProperty.LICENSE_INFO_IN_FILES, "licenseInfoInFiles"), + (FileProperty.NOTICE_TEXT, "noticeText")]) +def test_json_property_names(converter: FileConverter, file_property: FileProperty, expected: str): + assert converter.json_property_name(file_property) == expected + + +def test_json_type(converter: FileConverter): + assert converter.get_json_type() == FileProperty + + +def test_data_model_type(converter: FileConverter): + assert converter.get_data_model_type() == File + + +def test_successful_conversion(converter: FileConverter): + converter.checksum_converter.convert.return_value = "mock_converted_checksum" + converter.annotation_converter.convert.return_value = "mock_converted_annotation" + file = File(name="name", spdx_id="spdxId", + checksums=[Checksum(ChecksumAlgorithm.SHA224, "sha224"), Checksum(ChecksumAlgorithm.MD2, "md2")], + file_type=[FileType.SPDX, FileType.OTHER], concluded_license=LicenseExpression("licenseExpression1"), + license_info_in_file=[LicenseExpression("licenseExpression2"), LicenseExpression("licenseExpression3")], + license_comment="licenseComment", copyright_text="copyrightText", comment="comment", notice="notice", + contributors=["contributor1", "contributor2"], + attribution_texts=["attributionText1", "attributionText2"]) + + annotations = [Annotation(file.spdx_id, AnnotationType.REVIEW, Actor(ActorType.PERSON, "annotatorName"), + datetime(2022, 12, 5), "review comment")] + document = Document(creation_info_fixture(), files=[file], annotations=annotations) + + converted_dict = converter.convert(file, document) + + assert converted_dict == { + converter.json_property_name(FileProperty.SPDX_ID): "spdxId", + converter.json_property_name(FileProperty.ANNOTATIONS): ["mock_converted_annotation"], + converter.json_property_name(FileProperty.ATTRIBUTION_TEXTS): ["attributionText1", "attributionText2"], + converter.json_property_name(FileProperty.CHECKSUMS): ["mock_converted_checksum", "mock_converted_checksum"], + converter.json_property_name(FileProperty.COMMENT): "comment", + converter.json_property_name(FileProperty.COPYRIGHT_TEXT): "copyrightText", + converter.json_property_name(FileProperty.FILE_CONTRIBUTORS): ["contributor1", "contributor2"], + converter.json_property_name(FileProperty.FILE_NAME): "name", + converter.json_property_name(FileProperty.FILE_TYPES): ["SPDX", "OTHER"], + converter.json_property_name(FileProperty.LICENSE_COMMENTS): "licenseComment", + converter.json_property_name(FileProperty.LICENSE_CONCLUDED): "licenseExpression1", + converter.json_property_name(FileProperty.LICENSE_INFO_IN_FILES): ["licenseExpression2", "licenseExpression3"], + converter.json_property_name(FileProperty.NOTICE_TEXT): "notice" + } + + +def test_null_values(converter: FileConverter): + file = file_fixture(copyright_text=None, concluded_license=None, license_comment=None, comment=None, notice=None, + attribution_texts=[], checksums=[], contributors=[], file_type=[], license_info_in_file=[]) + document = Document(creation_info_fixture(), files=[file]) + + converted_dict = converter.convert(file, document) + + assert converter.json_property_name(FileProperty.COPYRIGHT_TEXT) not in converted_dict + assert converter.json_property_name(FileProperty.LICENSE_CONCLUDED) not in converted_dict + assert converter.json_property_name(FileProperty.LICENSE_COMMENTS) not in converted_dict + assert converter.json_property_name(FileProperty.COMMENT) not in converted_dict + assert converter.json_property_name(FileProperty.NOTICE_TEXT) not in converted_dict + assert converter.json_property_name(FileProperty.ANNOTATIONS) not in converted_dict + assert converter.json_property_name(FileProperty.ATTRIBUTION_TEXTS) not in converted_dict + assert converter.json_property_name(FileProperty.CHECKSUMS) not in converted_dict + assert converter.json_property_name(FileProperty.FILE_CONTRIBUTORS) not in converted_dict + assert converter.json_property_name(FileProperty.FILE_TYPES) not in converted_dict + assert converter.json_property_name(FileProperty.LICENSE_INFO_IN_FILES) not in converted_dict + + +def test_spdx_no_assertion(converter: FileConverter): + file = file_fixture(concluded_license=SpdxNoAssertion(), license_info_in_file=SpdxNoAssertion(), + copyright_text=SpdxNoAssertion()) + document = Document(creation_info_fixture(), files=[file]) + + converted_dict = converter.convert(file, document) + + assert converted_dict[ + converter.json_property_name(FileProperty.COPYRIGHT_TEXT)] == SPDX_NO_ASSERTION_STRING + assert converted_dict[converter.json_property_name(FileProperty.LICENSE_CONCLUDED)] == SPDX_NO_ASSERTION_STRING + assert converted_dict[converter.json_property_name(FileProperty.LICENSE_INFO_IN_FILES)] == SPDX_NO_ASSERTION_STRING + + +def test_spdx_none(converter: FileConverter): + file = file_fixture(concluded_license=SpdxNone(), license_info_in_file=SpdxNone(), copyright_text=SpdxNone()) + document = Document(creation_info_fixture(), files=[file]) + + converted_dict = converter.convert(file, document) + + assert converted_dict[ + converter.json_property_name(FileProperty.COPYRIGHT_TEXT)] == SPDX_NONE_STRING + assert converted_dict[converter.json_property_name(FileProperty.LICENSE_CONCLUDED)] == SPDX_NONE_STRING + assert converted_dict[converter.json_property_name(FileProperty.LICENSE_INFO_IN_FILES)] == SPDX_NONE_STRING + + +def test_file_annotations(converter: FileConverter): + file = file_fixture(spdx_id="fileId") + document = document_fixture(files=[file]) + first_file_annotation = annotation_fixture(spdx_id=file.spdx_id) + second_file_annotation = annotation_fixture(spdx_id=file.spdx_id) + document_annotation = annotation_fixture(spdx_id=document.creation_info.spdx_id) + package_annotation = annotation_fixture(spdx_id=document.packages[0].spdx_id) + snippet_annotation = annotation_fixture(spdx_id=document.snippets[0].spdx_id) + other_annotation = annotation_fixture(spdx_id="otherId") + annotations = [first_file_annotation, second_file_annotation, document_annotation, package_annotation, + snippet_annotation, other_annotation] + document.annotations = annotations + + # Weird type hint to make warnings about unresolved references from the mock class disappear + annotation_converter: Union[AnnotationConverter, NonCallableMagicMock] = converter.annotation_converter + annotation_converter.convert.return_value = "mock_converted_annotation" + + converted_dict = converter.convert(file, document) + + assert_mock_method_called_with_arguments(annotation_converter, "convert", first_file_annotation, + second_file_annotation) + converted_file_annotations = converted_dict.get(converter.json_property_name(FileProperty.ANNOTATIONS)) + assert converted_file_annotations == ["mock_converted_annotation", "mock_converted_annotation"] diff --git a/tests/jsonschema/test_package_converter.py b/tests/jsonschema/test_package_converter.py new file mode 100644 index 000000000..446cac7e5 --- /dev/null +++ b/tests/jsonschema/test_package_converter.py @@ -0,0 +1,279 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from datetime import datetime +from typing import Union +from unittest import mock +from unittest.mock import MagicMock, NonCallableMagicMock + +import pytest + +from src.jsonschema.annotation_converter import AnnotationConverter +from src.jsonschema.package_converter import PackageConverter +from src.jsonschema.package_properties import PackageProperty +from src.model.actor import Actor, ActorType +from src.model.annotation import Annotation, AnnotationType +from src.model.checksum import Checksum, ChecksumAlgorithm +from src.model.document import Document +from src.model.license_expression import LicenseExpression +from src.model.package import Package, PackageVerificationCode, PackagePurpose +from src.model.relationship import RelationshipType +from src.model.spdx_no_assertion import SpdxNoAssertion, SPDX_NO_ASSERTION_STRING +from src.model.spdx_none import SpdxNone, SPDX_NONE_STRING +from tests.fixtures import creation_info_fixture, package_fixture, external_package_ref_fixture, document_fixture, \ + annotation_fixture, file_fixture, relationship_fixture, snippet_fixture +from tests.mock_utils import assert_mock_method_called_with_arguments + + +@pytest.fixture +@mock.patch('src.jsonschema.checksum_converter.ChecksumConverter', autospec=True) +@mock.patch('src.jsonschema.annotation_converter.AnnotationConverter', autospec=True) +@mock.patch('src.jsonschema.package_verification_code_converter.PackageVerificationCodeConverter', autospec=True) +@mock.patch('src.jsonschema.external_package_ref_converter.ExternalPackageRefConverter', autospec=True) +def converter(package_ref_converter_mock: MagicMock, verification_code_converter_mock: MagicMock, + annotation_converter_mock: MagicMock, checksum_converter_mock: MagicMock) -> PackageConverter: + converter = PackageConverter() + converter.checksum_converter = checksum_converter_mock() + converter.annotation_converter = annotation_converter_mock() + converter.package_verification_code_converter = verification_code_converter_mock() + converter.external_package_ref_converter = package_ref_converter_mock() + return converter + + +@pytest.mark.parametrize("external_package_ref_property,expected", + [(PackageProperty.SPDX_ID, "SPDXID"), + (PackageProperty.ANNOTATIONS, "annotations"), + (PackageProperty.ATTRIBUTION_TEXTS, "attributionTexts"), + (PackageProperty.BUILT_DATE, "builtDate"), + (PackageProperty.CHECKSUMS, "checksums"), + (PackageProperty.COMMENT, "comment"), + (PackageProperty.COPYRIGHT_TEXT, "copyrightText"), + (PackageProperty.DESCRIPTION, "description"), + (PackageProperty.DOWNLOAD_LOCATION, "downloadLocation"), + (PackageProperty.EXTERNAL_REFS, "externalRefs"), + (PackageProperty.FILES_ANALYZED, "filesAnalyzed"), + (PackageProperty.HAS_FILES, "hasFiles"), + (PackageProperty.HOMEPAGE, "homepage"), + (PackageProperty.LICENSE_COMMENTS, "licenseComments"), + (PackageProperty.LICENSE_CONCLUDED, "licenseConcluded"), + (PackageProperty.LICENSE_DECLARED, "licenseDeclared"), + (PackageProperty.LICENSE_INFO_FROM_FILES, "licenseInfoFromFiles"), + (PackageProperty.NAME, "name"), + (PackageProperty.ORIGINATOR, "originator"), + (PackageProperty.PACKAGE_FILE_NAME, "packageFileName"), + (PackageProperty.PACKAGE_VERIFICATION_CODE, "packageVerificationCode"), + (PackageProperty.PRIMARY_PACKAGE_PURPOSE, "primaryPackagePurpose"), + (PackageProperty.RELEASE_DATE, "releaseDate"), + (PackageProperty.SOURCE_INFO, "sourceInfo"), + (PackageProperty.SUMMARY, "summary"), + (PackageProperty.SUPPLIER, "supplier"), + (PackageProperty.VALID_UNTIL_DATE, "validUntilDate"), + (PackageProperty.VERSION_INFO, "versionInfo")]) +def test_json_property_names(converter: PackageConverter, + external_package_ref_property: PackageProperty, expected: str): + assert converter.json_property_name(external_package_ref_property) == expected + + +def test_json_type(converter: PackageConverter): + assert converter.get_json_type() == PackageProperty + + +def test_data_model_type(converter: PackageConverter): + assert converter.get_data_model_type() == Package + + +def test_successful_conversion(converter: PackageConverter): + converter.checksum_converter.convert.return_value = "mock_converted_checksum" + converter.annotation_converter.convert.return_value = "mock_converted_annotation" + converter.package_verification_code_converter.convert.return_value = "mock_converted_verification_code" + converter.external_package_ref_converter.convert.return_value = "mock_package_ref" + package = Package(spdx_id="packageId", name="name", download_location="downloadLocation", version="version", + file_name="fileName", supplier=Actor(ActorType.PERSON, "supplierName"), + originator=Actor(ActorType.PERSON, "originatorName"), files_analyzed=True, + verification_code=PackageVerificationCode("value"), + checksums=[Checksum(ChecksumAlgorithm.SHA1, "sha1"), + Checksum(ChecksumAlgorithm.BLAKE2B_256, "blake")], homepage="homepage", + source_info="sourceInfo", license_concluded=LicenseExpression("licenseExpression1"), + license_info_from_files=[LicenseExpression("licenseExpression2"), + LicenseExpression("licenseExpression3")], + license_declared=LicenseExpression("licenseExpression4"), license_comment="licenseComment", + copyright_text="copyrightText", summary="summary", description="description", comment="comment", + external_references=[external_package_ref_fixture()], + attribution_texts=["attributionText1", "attributionText2"], + primary_package_purpose=PackagePurpose.APPLICATION, release_date=datetime(2022, 12, 1), + built_date=datetime(2022, 12, 2), valid_until_date=datetime(2022, 12, 3)) + + annotation = Annotation(package.spdx_id, AnnotationType.REVIEW, Actor(ActorType.TOOL, "toolName"), + datetime(2022, 12, 5), "review comment") + document = Document(creation_info_fixture(), packages=[package], annotations=[annotation]) + + converted_dict = converter.convert(package, document) + + assert converted_dict == { + converter.json_property_name(PackageProperty.SPDX_ID): "packageId", + converter.json_property_name(PackageProperty.ANNOTATIONS): ["mock_converted_annotation"], + converter.json_property_name(PackageProperty.ATTRIBUTION_TEXTS): ["attributionText1", "attributionText2"], + converter.json_property_name(PackageProperty.NAME): "name", + converter.json_property_name(PackageProperty.DOWNLOAD_LOCATION): "downloadLocation", + converter.json_property_name(PackageProperty.VERSION_INFO): "version", + converter.json_property_name(PackageProperty.PACKAGE_FILE_NAME): "fileName", + converter.json_property_name(PackageProperty.SUPPLIER): "Person: supplierName", + converter.json_property_name(PackageProperty.ORIGINATOR): "Person: originatorName", + converter.json_property_name(PackageProperty.FILES_ANALYZED): True, + converter.json_property_name(PackageProperty.PACKAGE_VERIFICATION_CODE): "mock_converted_verification_code", + converter.json_property_name(PackageProperty.CHECKSUMS): ["mock_converted_checksum", "mock_converted_checksum"], + converter.json_property_name(PackageProperty.HOMEPAGE): "homepage", + converter.json_property_name(PackageProperty.SOURCE_INFO): "sourceInfo", + converter.json_property_name(PackageProperty.LICENSE_CONCLUDED): "licenseExpression1", + converter.json_property_name(PackageProperty.LICENSE_INFO_FROM_FILES): ["licenseExpression2", + "licenseExpression3"], + converter.json_property_name(PackageProperty.LICENSE_DECLARED): "licenseExpression4", + converter.json_property_name(PackageProperty.LICENSE_COMMENTS): "licenseComment", + converter.json_property_name(PackageProperty.COPYRIGHT_TEXT): "copyrightText", + converter.json_property_name(PackageProperty.SUMMARY): "summary", + converter.json_property_name(PackageProperty.DESCRIPTION): "description", + converter.json_property_name(PackageProperty.COMMENT): "comment", + converter.json_property_name(PackageProperty.EXTERNAL_REFS): ["mock_package_ref"], + converter.json_property_name(PackageProperty.PRIMARY_PACKAGE_PURPOSE): "APPLICATION", + converter.json_property_name(PackageProperty.RELEASE_DATE): "2022-12-01T00:00:00Z", + converter.json_property_name(PackageProperty.BUILT_DATE): "2022-12-02T00:00:00Z", + converter.json_property_name(PackageProperty.VALID_UNTIL_DATE): "2022-12-03T00:00:00Z" + } + + +def test_null_values(converter: PackageConverter): + package = package_fixture(built_date=None, release_date=None, valid_until_date=None, homepage=None, + license_concluded=None, license_declared=None, originator=None, verification_code=None, + primary_package_purpose=None, supplier=None, version=None, file_name=None, + source_info=None, license_comment=None, copyright_text=None, summary=None, + description=None, comment=None, attribution_texts=[], checksums=[], + external_references=[], license_info_from_files=[]) + + document = Document(creation_info_fixture(), packages=[package]) + + converted_dict = converter.convert(package, document) + + assert converter.json_property_name(PackageProperty.VERSION_INFO) not in converted_dict + assert converter.json_property_name(PackageProperty.PACKAGE_FILE_NAME) not in converted_dict + assert converter.json_property_name(PackageProperty.SUPPLIER) not in converted_dict + assert converter.json_property_name(PackageProperty.ORIGINATOR) not in converted_dict + assert converter.json_property_name(PackageProperty.PACKAGE_VERIFICATION_CODE) not in converted_dict + assert converter.json_property_name(PackageProperty.HOMEPAGE) not in converted_dict + assert converter.json_property_name(PackageProperty.SOURCE_INFO) not in converted_dict + assert converter.json_property_name(PackageProperty.LICENSE_CONCLUDED) not in converted_dict + assert converter.json_property_name(PackageProperty.LICENSE_DECLARED) not in converted_dict + assert converter.json_property_name(PackageProperty.LICENSE_COMMENTS) not in converted_dict + assert converter.json_property_name(PackageProperty.COPYRIGHT_TEXT) not in converted_dict + assert converter.json_property_name(PackageProperty.SUMMARY) not in converted_dict + assert converter.json_property_name(PackageProperty.DESCRIPTION) not in converted_dict + assert converter.json_property_name(PackageProperty.COMMENT) not in converted_dict + assert converter.json_property_name(PackageProperty.PRIMARY_PACKAGE_PURPOSE) not in converted_dict + assert converter.json_property_name(PackageProperty.BUILT_DATE) not in converted_dict + assert converter.json_property_name(PackageProperty.RELEASE_DATE) not in converted_dict + assert converter.json_property_name(PackageProperty.VALID_UNTIL_DATE) not in converted_dict + assert converter.json_property_name(PackageProperty.ANNOTATIONS) not in converted_dict + assert converter.json_property_name(PackageProperty.ATTRIBUTION_TEXTS) not in converted_dict + assert converter.json_property_name(PackageProperty.CHECKSUMS) not in converted_dict + assert converter.json_property_name(PackageProperty.EXTERNAL_REFS) not in converted_dict + assert converter.json_property_name(PackageProperty.HAS_FILES) not in converted_dict + assert converter.json_property_name(PackageProperty.LICENSE_INFO_FROM_FILES) not in converted_dict + + +def test_spdx_no_assertion(converter: PackageConverter): + package = package_fixture(download_location=SpdxNoAssertion(), supplier=SpdxNoAssertion(), + originator=SpdxNoAssertion(), homepage=SpdxNoAssertion(), + license_concluded=SpdxNoAssertion(), license_info_from_files=SpdxNoAssertion(), + license_declared=SpdxNoAssertion(), copyright_text=SpdxNoAssertion()) + + document = Document(creation_info_fixture(), packages=[package]) + + converted_dict = converter.convert(package, document) + + assert converted_dict[converter.json_property_name(PackageProperty.DOWNLOAD_LOCATION)] == SPDX_NO_ASSERTION_STRING + assert converted_dict[converter.json_property_name(PackageProperty.SUPPLIER)] == SPDX_NO_ASSERTION_STRING + assert converted_dict[converter.json_property_name(PackageProperty.ORIGINATOR)] == SPDX_NO_ASSERTION_STRING + assert converted_dict[converter.json_property_name(PackageProperty.HOMEPAGE)] == SPDX_NO_ASSERTION_STRING + assert converted_dict[converter.json_property_name(PackageProperty.LICENSE_CONCLUDED)] == SPDX_NO_ASSERTION_STRING + assert converted_dict[ + converter.json_property_name(PackageProperty.LICENSE_INFO_FROM_FILES)] == SPDX_NO_ASSERTION_STRING + assert converted_dict[converter.json_property_name(PackageProperty.LICENSE_DECLARED)] == SPDX_NO_ASSERTION_STRING + assert converted_dict[converter.json_property_name(PackageProperty.COPYRIGHT_TEXT)] == SPDX_NO_ASSERTION_STRING + + +def test_spdx_none(converter: PackageConverter): + package = package_fixture(download_location=SpdxNone(), homepage=SpdxNone(), + license_concluded=SpdxNone(), license_info_from_files=SpdxNone(), + license_declared=SpdxNone(), copyright_text=SpdxNone()) + + document = Document(creation_info_fixture(), packages=[package]) + + converted_dict = converter.convert(package, document) + + assert converted_dict[converter.json_property_name(PackageProperty.DOWNLOAD_LOCATION)] == SPDX_NONE_STRING + assert converted_dict[converter.json_property_name(PackageProperty.HOMEPAGE)] == SPDX_NONE_STRING + assert converted_dict[converter.json_property_name(PackageProperty.LICENSE_CONCLUDED)] == SPDX_NONE_STRING + assert converted_dict[converter.json_property_name(PackageProperty.LICENSE_INFO_FROM_FILES)] == SPDX_NONE_STRING + assert converted_dict[converter.json_property_name(PackageProperty.LICENSE_DECLARED)] == SPDX_NONE_STRING + assert converted_dict[converter.json_property_name(PackageProperty.COPYRIGHT_TEXT)] == SPDX_NONE_STRING + + +def test_package_annotations(converter: PackageConverter): + package = package_fixture(spdx_id="packageId") + document = document_fixture(packages=[package]) + first_package_annotation = annotation_fixture(spdx_id=package.spdx_id) + second_package_annotation = annotation_fixture(spdx_id=package.spdx_id) + document_annotation = annotation_fixture(spdx_id=document.creation_info.spdx_id) + file_annotation = annotation_fixture(spdx_id=document.files[0].spdx_id) + snippet_annotation = annotation_fixture(spdx_id=document.snippets[0].spdx_id) + other_annotation = annotation_fixture(spdx_id="otherId") + annotations = [first_package_annotation, second_package_annotation, document_annotation, file_annotation, + snippet_annotation, other_annotation] + document.annotations = annotations + + # Weird type hint to make warnings about unresolved references from the mock class disappear + annotation_converter: Union[AnnotationConverter, NonCallableMagicMock] = converter.annotation_converter + annotation_converter.convert.return_value = "mock_converted_annotation" + + converted_dict = converter.convert(package, document) + + assert_mock_method_called_with_arguments(annotation_converter, "convert", first_package_annotation, + second_package_annotation) + converted_file_annotations = converted_dict.get(converter.json_property_name(PackageProperty.ANNOTATIONS)) + assert converted_file_annotations == ["mock_converted_annotation", "mock_converted_annotation"] + + +def test_has_files(converter: PackageConverter): + package = package_fixture() + first_contained_file = file_fixture(spdx_id="firstFileId") + second_contained_file = file_fixture(spdx_id="secondFileId") + non_contained_file = file_fixture(spdx_id="otherFileId") + snippet = snippet_fixture() + document = document_fixture(packages=[package], + files=[first_contained_file, second_contained_file, non_contained_file], + snippets=[snippet]) + package_contains_file_relationship = relationship_fixture(spdx_element_id=package.spdx_id, + relationship_type=RelationshipType.CONTAINS, + related_spdx_element_id=first_contained_file.spdx_id) + file_contained_in_package_relationship = relationship_fixture(spdx_element_id=second_contained_file.spdx_id, + relationship_type=RelationshipType.CONTAINED_BY, + related_spdx_element_id=package.spdx_id) + package_contains_snippet_relationship = relationship_fixture(spdx_element_id=package.spdx_id, + relationship_type=RelationshipType.CONTAINS, + related_spdx_element_id=snippet.spdx_id) + package_describes_file_relationship = relationship_fixture(spdx_element_id=package.spdx_id, + relationship_type=RelationshipType.DESCRIBES, + related_spdx_element_id=non_contained_file.spdx_id) + document.relationships = [package_contains_file_relationship, file_contained_in_package_relationship, + package_contains_snippet_relationship, package_describes_file_relationship] + + converted_dict = converter.convert(package, document) + + has_files = converted_dict.get(converter.json_property_name(PackageProperty.HAS_FILES)) + assert has_files == [first_contained_file.spdx_id, second_contained_file.spdx_id] diff --git a/tests/jsonschema/test_package_verification_code_converter.py b/tests/jsonschema/test_package_verification_code_converter.py new file mode 100644 index 000000000..a7050c492 --- /dev/null +++ b/tests/jsonschema/test_package_verification_code_converter.py @@ -0,0 +1,59 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +from src.jsonschema.package_verification_code_converter import PackageVerificationCodeConverter +from src.jsonschema.package_verification_code_properties import PackageVerificationCodeProperty +from src.model.package import PackageVerificationCode + + +@pytest.fixture +def converter() -> PackageVerificationCodeConverter: + return PackageVerificationCodeConverter() + + +@pytest.mark.parametrize("package_verification_code_property,expected", + [(PackageVerificationCodeProperty.PACKAGE_VERIFICATION_CODE_EXCLUDED_FILES, + "packageVerificationCodeExcludedFiles"), + (PackageVerificationCodeProperty.PACKAGE_VERIFICATION_CODE_VALUE, + "packageVerificationCodeValue")]) +def test_json_property_names(converter: PackageVerificationCodeConverter, + package_verification_code_property: PackageVerificationCodeProperty, expected: str): + assert converter.json_property_name(package_verification_code_property) == expected + + +def test_json_type(converter: PackageVerificationCodeConverter): + assert converter.get_json_type() == PackageVerificationCodeProperty + + +def test_data_model_type(converter: PackageVerificationCodeConverter): + assert converter.get_data_model_type() == PackageVerificationCode + + +def test_successful_conversion(converter: PackageVerificationCodeConverter): + package_verification_code = PackageVerificationCode("value", ["file1", "file2"]) + + converted_dict = converter.convert(package_verification_code) + + assert converted_dict == { + converter.json_property_name(PackageVerificationCodeProperty.PACKAGE_VERIFICATION_CODE_EXCLUDED_FILES): [ + "file1", "file2"], + converter.json_property_name(PackageVerificationCodeProperty.PACKAGE_VERIFICATION_CODE_VALUE): "value" + } + + +def test_null_values(converter: PackageVerificationCodeConverter): + package_verification_code = PackageVerificationCode("value") + + converted_dict = converter.convert(package_verification_code) + + assert converter.json_property_name( + PackageVerificationCodeProperty.PACKAGE_VERIFICATION_CODE_EXCLUDED_FILES) not in converted_dict diff --git a/tests/jsonschema/test_relationship_converter.py b/tests/jsonschema/test_relationship_converter.py new file mode 100644 index 000000000..8646e4dcc --- /dev/null +++ b/tests/jsonschema/test_relationship_converter.py @@ -0,0 +1,71 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +from src.jsonschema.relationship_converter import RelationshipConverter +from src.jsonschema.relationship_properties import RelationshipProperty +from src.model.relationship import Relationship, RelationshipType +from src.model.spdx_no_assertion import SpdxNoAssertion, SPDX_NO_ASSERTION_STRING +from src.model.spdx_none import SpdxNone, SPDX_NONE_STRING +from tests.fixtures import relationship_fixture + + +@pytest.fixture +def converter() -> RelationshipConverter: + return RelationshipConverter() + + +@pytest.mark.parametrize("relationship_property,expected", + [(RelationshipProperty.SPDX_ELEMENT_ID, "spdxElementId"), + (RelationshipProperty.COMMENT, "comment"), + (RelationshipProperty.RELATED_SPDX_ELEMENT, "relatedSpdxElement"), + (RelationshipProperty.RELATIONSHIP_TYPE, "relationshipType")]) +def test_json_property_names(converter: RelationshipConverter, relationship_property: RelationshipProperty, + expected: str): + assert converter.json_property_name(relationship_property) == expected + + +def test_json_type(converter: RelationshipConverter): + assert converter.get_json_type() == RelationshipProperty + + +def test_data_model_type(converter: RelationshipConverter): + assert converter.get_data_model_type() == Relationship + + +def test_successful_conversion(converter: RelationshipConverter): + relationship = Relationship("spdxElementId", RelationshipType.COPY_OF, "relatedElementId", "comment") + + converted_dict = converter.convert(relationship) + + assert converted_dict == { + converter.json_property_name(RelationshipProperty.SPDX_ELEMENT_ID): "spdxElementId", + converter.json_property_name(RelationshipProperty.COMMENT): "comment", + converter.json_property_name(RelationshipProperty.RELATED_SPDX_ELEMENT): "relatedElementId", + converter.json_property_name(RelationshipProperty.RELATIONSHIP_TYPE): "COPY_OF" + } + + +def test_spdx_no_assertion(converter: RelationshipConverter): + relationship = relationship_fixture(related_spdx_element_id=SpdxNoAssertion()) + + converted_dict = converter.convert(relationship) + + assert converted_dict[ + converter.json_property_name(RelationshipProperty.RELATED_SPDX_ELEMENT)] == SPDX_NO_ASSERTION_STRING + + +def test_spdx_none(converter: RelationshipConverter): + relationship = relationship_fixture(related_spdx_element_id=SpdxNone()) + + converted_dict = converter.convert(relationship) + + assert converted_dict[converter.json_property_name(RelationshipProperty.RELATED_SPDX_ELEMENT)] == SPDX_NONE_STRING diff --git a/tests/jsonschema/test_snippet_converter.py b/tests/jsonschema/test_snippet_converter.py new file mode 100644 index 000000000..75432235c --- /dev/null +++ b/tests/jsonschema/test_snippet_converter.py @@ -0,0 +1,161 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from datetime import datetime +from typing import Union +from unittest import mock +from unittest.mock import MagicMock, NonCallableMagicMock + +import pytest + +from src.jsonschema.annotation_converter import AnnotationConverter +from src.jsonschema.snippet_converter import SnippetConverter +from src.jsonschema.snippet_properties import SnippetProperty +from src.model.actor import Actor, ActorType +from src.model.annotation import Annotation, AnnotationType +from src.model.document import Document +from src.model.license_expression import LicenseExpression +from src.model.snippet import Snippet +from src.model.spdx_no_assertion import SpdxNoAssertion, SPDX_NO_ASSERTION_STRING +from src.model.spdx_none import SpdxNone, SPDX_NONE_STRING +from tests.fixtures import creation_info_fixture, snippet_fixture, document_fixture, annotation_fixture +from tests.mock_utils import assert_mock_method_called_with_arguments + + +@pytest.fixture +@mock.patch('src.jsonschema.annotation_converter.AnnotationConverter', autospec=True) +def converter(annotation_converter_mock: MagicMock) -> SnippetConverter: + converter = SnippetConverter() + converter.annotation_converter = annotation_converter_mock() + return converter + + +@pytest.mark.parametrize("snippet_property,expected", + [(SnippetProperty.SPDX_ID, "SPDXID"), + (SnippetProperty.ANNOTATIONS, "annotations"), + (SnippetProperty.ATTRIBUTION_TEXTS, "attributionTexts"), + (SnippetProperty.COMMENT, "comment"), + (SnippetProperty.COPYRIGHT_TEXT, "copyrightText"), + (SnippetProperty.LICENSE_COMMENTS, "licenseComments"), + (SnippetProperty.LICENSE_CONCLUDED, "licenseConcluded"), + (SnippetProperty.LICENSE_INFO_IN_SNIPPETS, "licenseInfoInSnippets"), + (SnippetProperty.NAME, "name"), + (SnippetProperty.RANGES, "ranges"), + (SnippetProperty.SNIPPET_FROM_FILE, "snippetFromFile")]) +def test_json_property_names(converter: SnippetConverter, snippet_property: SnippetProperty, + expected: str): + assert converter.json_property_name(snippet_property) == expected + + +def test_json_type(converter: SnippetConverter): + assert converter.get_json_type() == SnippetProperty + + +def test_data_model_type(converter: SnippetConverter): + assert converter.get_data_model_type() == Snippet + + +def test_successful_conversion(converter: SnippetConverter): + converter.annotation_converter.convert.return_value = "mock_converted_annotation" + file_spdx_id = "fileSpdxId" + snippet = Snippet("spdxId", file_spdx_id=file_spdx_id, byte_range=(1, 2), line_range=(3, 4), + concluded_license=LicenseExpression("licenseExpression1"), + license_info_in_snippet=[LicenseExpression("licenseExpression2"), + LicenseExpression("licenseExpression3")], + license_comment="licenseComment", copyright_text="copyrightText", comment="comment", name="name", + attribution_texts=["attributionText1", "attributionText2"]) + + annotation = Annotation(snippet.spdx_id, AnnotationType.OTHER, Actor(ActorType.PERSON, "annotatorName"), + datetime(2022, 12, 5), "other comment") + document = Document(creation_info_fixture(), snippets=[snippet], annotations=[annotation]) + converted_dict = converter.convert(snippet, document) + + assert converted_dict == { + converter.json_property_name(SnippetProperty.SPDX_ID): "spdxId", + converter.json_property_name(SnippetProperty.ANNOTATIONS): ["mock_converted_annotation"], + converter.json_property_name(SnippetProperty.ATTRIBUTION_TEXTS): ["attributionText1", "attributionText2"], + converter.json_property_name(SnippetProperty.COMMENT): "comment", + converter.json_property_name(SnippetProperty.COPYRIGHT_TEXT): "copyrightText", + converter.json_property_name(SnippetProperty.LICENSE_COMMENTS): "licenseComment", + converter.json_property_name(SnippetProperty.LICENSE_CONCLUDED): "licenseExpression1", + converter.json_property_name(SnippetProperty.LICENSE_INFO_IN_SNIPPETS): ["licenseExpression2", + "licenseExpression3"], + converter.json_property_name(SnippetProperty.NAME): "name", + converter.json_property_name(SnippetProperty.RANGES): [ + {"startPointer": {"reference": file_spdx_id, "offset": 1}, + "endPointer": {"reference": file_spdx_id, "offset": 2}}, + {"startPointer": {"reference": file_spdx_id, "lineNumber": 3}, + "endPointer": {"reference": file_spdx_id, "lineNumber": 4}}], + converter.json_property_name(SnippetProperty.SNIPPET_FROM_FILE): file_spdx_id + } + + +def test_null_values(converter: SnippetConverter): + snippet = snippet_fixture(concluded_license=None, license_comment=None, copyright_text=None, comment=None, + name=None, attribution_texts=[], license_info_in_snippet=[]) + + document = Document(creation_info_fixture(), snippets=[snippet]) + converted_dict = converter.convert(snippet, document) + + assert converter.json_property_name(SnippetProperty.LICENSE_CONCLUDED) not in converted_dict + assert converter.json_property_name(SnippetProperty.LICENSE_COMMENTS) not in converted_dict + assert converter.json_property_name(SnippetProperty.COPYRIGHT_TEXT) not in converted_dict + assert converter.json_property_name(SnippetProperty.COMMENT) not in converted_dict + assert converter.json_property_name(SnippetProperty.NAME) not in converted_dict + assert converter.json_property_name(SnippetProperty.ANNOTATIONS) not in converted_dict + assert converter.json_property_name(SnippetProperty.ATTRIBUTION_TEXTS) not in converted_dict + assert converter.json_property_name(SnippetProperty.LICENSE_INFO_IN_SNIPPETS) not in converted_dict + + +def test_spdx_no_assertion(converter: SnippetConverter): + snippet = snippet_fixture(concluded_license=SpdxNoAssertion(), license_info_in_snippet=SpdxNoAssertion()) + + document = Document(creation_info_fixture(), snippets=[snippet]) + converted_dict = converter.convert(snippet, document) + + assert converted_dict[converter.json_property_name(SnippetProperty.LICENSE_CONCLUDED)] == SPDX_NO_ASSERTION_STRING + assert converted_dict[ + converter.json_property_name(SnippetProperty.LICENSE_INFO_IN_SNIPPETS)] == SPDX_NO_ASSERTION_STRING + + +def test_spdx_none(converter: SnippetConverter): + snippet = snippet_fixture(concluded_license=SpdxNone(), license_info_in_snippet=SpdxNone()) + + document = Document(creation_info_fixture(), snippets=[snippet]) + converted_dict = converter.convert(snippet, document) + + assert converted_dict[converter.json_property_name(SnippetProperty.LICENSE_CONCLUDED)] == SPDX_NONE_STRING + assert converted_dict[ + converter.json_property_name(SnippetProperty.LICENSE_INFO_IN_SNIPPETS)] == SPDX_NONE_STRING + + +def test_snippet_annotations(converter: SnippetConverter): + snippet = snippet_fixture(spdx_id="snippetId") + document = document_fixture(snippets=[snippet]) + first_snippet_annotation = annotation_fixture(spdx_id=snippet.spdx_id) + second_snippet_annotation = annotation_fixture(spdx_id=snippet.spdx_id) + document_annotation = annotation_fixture(spdx_id=document.creation_info.spdx_id) + package_annotation = annotation_fixture(spdx_id=document.packages[0].spdx_id) + file_annotation = annotation_fixture(spdx_id=document.files[0].spdx_id) + other_annotation = annotation_fixture(spdx_id="otherId") + annotations = [first_snippet_annotation, second_snippet_annotation, document_annotation, package_annotation, + file_annotation, other_annotation] + document.annotations = annotations + + # Weird type hint to make warnings about unresolved references from the mock class disappear + annotation_converter: Union[AnnotationConverter, NonCallableMagicMock] = converter.annotation_converter + annotation_converter.convert.return_value = "mock_converted_annotation" + + converted_dict = converter.convert(snippet, document) + + assert_mock_method_called_with_arguments(annotation_converter, "convert", first_snippet_annotation, + second_snippet_annotation) + converted_file_annotations = converted_dict.get(converter.json_property_name(SnippetProperty.ANNOTATIONS)) + assert converted_file_annotations == ["mock_converted_annotation", "mock_converted_annotation"] diff --git a/tests/mock_utils.py b/tests/mock_utils.py new file mode 100644 index 000000000..09af9f19e --- /dev/null +++ b/tests/mock_utils.py @@ -0,0 +1,23 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from unittest.mock import NonCallableMagicMock + + +def assert_mock_method_called_with_arguments(mock_object: NonCallableMagicMock, method_name: str, *args): + assert len(mock_object.method_calls) == len(args) + for running_index in range(len(args)): + call = mock_object.method_calls[running_index] + assert call[0] == method_name + assert call[1][0] == args[running_index] + + +def assert_no_mock_methods_called(mock_object: NonCallableMagicMock): + assert len(mock_object.method_calls) == 0 diff --git a/tests/model/test_actor.py b/tests/model/test_actor.py index f1eacf5b1..c39ac744a 100644 --- a/tests/model/test_actor.py +++ b/tests/model/test_actor.py @@ -36,3 +36,16 @@ def test_wrong_type_in_email_after_initializing(): with pytest.raises(TypeError): actor = Actor(ActorType.PERSON, "name") actor.email = [] + + +@pytest.mark.parametrize("actor,expected_string", [(Actor(ActorType.PERSON, "personName"), "Person: personName"), + (Actor(ActorType.PERSON, "personName", "personEmail"), + "Person: personName (personEmail)"), + (Actor(ActorType.ORGANIZATION, "orgName"), "Organization: orgName"), + (Actor(ActorType.ORGANIZATION, "orgName", "orgEmail"), + "Organization: orgName (orgEmail)"), + (Actor(ActorType.TOOL, "toolName"), "Tool: toolName"), + (Actor(ActorType.TOOL, "toolName", "toolEmail"), + "Tool: toolName (toolEmail)")]) +def test_serialization(actor: Actor, expected_string: str): + assert actor.to_serialized_string() == expected_string diff --git a/tests/test_datetime_conversions.py b/tests/test_datetime_conversions.py index 6ec233cda..e70e1dc86 100644 --- a/tests/test_datetime_conversions.py +++ b/tests/test_datetime_conversions.py @@ -12,7 +12,11 @@ import pytest -from src.datetime_conversions import datetime_from_str +from src.datetime_conversions import datetime_from_str, datetime_to_iso_string + + +def test_datetime_to_iso_string(): + assert datetime_to_iso_string(datetime(2022, 12, 13, 1, 2, 3)) == "2022-12-13T01:02:03Z" def test_datetime_from_str(): diff --git a/tests/writer/__init__.py b/tests/writer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/writer/json/__init__.py b/tests/writer/json/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/writer/json/expected_results/__init__.py b/tests/writer/json/expected_results/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/writer/json/expected_results/expected.json b/tests/writer/json/expected_results/expected.json new file mode 100644 index 000000000..3335e12eb --- /dev/null +++ b/tests/writer/json/expected_results/expected.json @@ -0,0 +1,101 @@ +{ + "SPDXID": "documentId", + "annotations": [ + { + "annotationDate": "2022-12-02T00:00:00Z", + "annotationType": "REVIEW", + "annotator": "Person: reviewerName", + "comment": "reviewComment" + } + ], + "comment": "comment", + "creationInfo": { + "created": "2022-12-01T00:00:00Z", + "creators": [ + "Tool: tools-python (tools-python@github.com)" + ] + }, + "dataLicense": "dataLicense", + "externalDocumentRefs": [ + { + "externalDocumentId": "docRefId", + "spdxDocument": "externalDocumentUri", + "checksum": { + "algorithm": "SHA1", + "checksumValue": "externalRefSha1" + } + } + ], + "hasExtractedLicensingInfo": [ + { + "extractedText": "licenseText", + "licenseId": "licenseId" + } + ], + "name": "documentName", + "spdxVersion": "spdxVersion", + "documentNamespace": "documentNamespace", + "documentDescribes": [ + "packageId", + "fileId" + ], + "packages": [ + { + "SPDXID": "packageId", + "downloadLocation": "NONE", + "filesAnalyzed": true, + "name": "packageName" + } + ], + "files": [ + { + "SPDXID": "fileId", + "annotations": [ + { + "annotationDate": "2022-12-03T00:00:00Z", + "annotationType": "OTHER", + "annotator": "Tool: toolName", + "comment": "otherComment" + } + ], + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "fileSha1" + } + ], + "fileName": "fileName" + } + ], + "snippets": [ + { + "SPDXID": "snippetId", + "ranges": [ + { + "startPointer": { + "reference": "snippetFileId", + "offset": 1 + }, + "endPointer": { + "reference": "snippetFileId", + "offset": 2 + } + } + ], + "snippetFromFile": "snippetFileId" + } + ], + "relationships": [ + { + "spdxElementId": "documentId", + "comment": "relationshipComment", + "relatedSpdxElement": "fileId", + "relationshipType": "DESCRIBES" + }, + { + "spdxElementId": "relationshipOriginId", + "relatedSpdxElement": "relationShipTargetId", + "relationshipType": "AMENDS" + } + ] +} diff --git a/tests/writer/json/test_json_writer.py b/tests/writer/json/test_json_writer.py new file mode 100644 index 000000000..6e48cf5da --- /dev/null +++ b/tests/writer/json/test_json_writer.py @@ -0,0 +1,86 @@ +# Copyright (c) 2022 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +import os +from datetime import datetime + +import pytest + +from src.model.actor import Actor, ActorType +from src.model.annotation import Annotation, AnnotationType +from src.model.checksum import ChecksumAlgorithm, Checksum +from src.model.document import CreationInfo, Document +from src.model.external_document_ref import ExternalDocumentRef +from src.model.extracted_licensing_info import ExtractedLicensingInfo +from src.model.file import File +from src.model.package import Package +from src.model.relationship import RelationshipType, Relationship +from src.model.snippet import Snippet +from src.model.spdx_none import SpdxNone +from src.writer.json.json_writer import write_document +from tests.fixtures import document_fixture + + +@pytest.fixture +def temporary_file_path() -> str: + temporary_file_path = "temp_test_json_writer_output.json" + yield temporary_file_path + os.remove(temporary_file_path) + + +def test_write_json(temporary_file_path: str): + creation_info = CreationInfo("spdxVersion", "documentId", "documentName", "documentNamespace", + [Actor(ActorType.TOOL, "tools-python", "tools-python@github.com")], + datetime(2022, 12, 1), document_comment="comment", data_license="dataLicense", + external_document_refs=[ExternalDocumentRef("docRefId", "externalDocumentUri", + Checksum(ChecksumAlgorithm.SHA1, + "externalRefSha1"))]) + package = Package("packageId", "packageName", SpdxNone()) + file = File("fileName", "fileId", [Checksum(ChecksumAlgorithm.SHA1, "fileSha1")]) + snippet = Snippet("snippetId", "snippetFileId", (1, 2)) + relationships = [ + Relationship(creation_info.spdx_id, RelationshipType.DESCRIBES, "packageId"), + Relationship(creation_info.spdx_id, RelationshipType.DESCRIBES, "fileId", "relationshipComment"), + Relationship("relationshipOriginId", RelationshipType.AMENDS, "relationShipTargetId")] + annotations = [ + Annotation("documentId", AnnotationType.REVIEW, Actor(ActorType.PERSON, "reviewerName"), + datetime(2022, 12, 2), "reviewComment"), + Annotation("fileId", AnnotationType.OTHER, Actor(ActorType.TOOL, "toolName"), datetime(2022, 12, 3), + "otherComment")] + extracted_licensing_info = [ExtractedLicensingInfo("licenseId", "licenseText")] + document = Document(creation_info, annotations=annotations, extracted_licensing_info=extracted_licensing_info, + relationships=relationships, packages=[package], files=[file], snippets=[snippet]) + # TODO: Enable validation once test data is valid, https://github.com/spdx/tools-python/issues/397 + write_document(document, temporary_file_path, validate=False) + + with open(temporary_file_path) as written_file: + written_json = json.load(written_file) + + with open(os.path.join(os.path.dirname(__file__), 'expected_results', 'expected.json')) as expected_file: + expected_json = json.load(expected_file) + + assert written_json == expected_json + + +def test_document_is_validated(): + document = document_fixture() + document.creation_info.spdx_id = "InvalidId" + + with pytest.raises(ValueError) as error: + write_document(document, "dummy_path") + assert "Document is not valid" in error.value.args[0] + + +def test_document_validation_can_be_overridden(temporary_file_path: str): + document = document_fixture() + document.creation_info.spdx_id = "InvalidId" + + write_document(document, temporary_file_path, validate=False)