Repository: spdx/tools-python

Issue:
Test `spdx_id_validators`
Test the methods found in `src/validation/spdx_id_validators`.
A dummy file already exists at `tests/validation/test_spdx_id_validators`.

Comments:

Patch:
diff --git a/src/spdx/validation/document_validator.py b/src/spdx/validation/document_validator.py
index a3fa007..fe6422a 100644
--- a/src/spdx/validation/document_validator.py
+++ b/src/spdx/validation/document_validator.py
@@ -21,6 +21,7 @@ from spdx.validation.file_validator import validate_files
 from spdx.validation.package_validator import validate_packages
 from spdx.validation.relationship_validator import validate_relationships
 from spdx.validation.snippet_validator import validate_snippets
+from spdx.validation.spdx_id_validators import get_list_of_all_spdx_ids
 from spdx.validation.validation_message import ValidationMessage, ValidationContext, SpdxElementType
 
 
@@ -74,4 +75,16 @@ def validate_full_spdx_document(document: Document, spdx_version: str = None) ->
                 ValidationContext(spdx_id=document_id,
                                   element_type=SpdxElementType.DOCUMENT)))
 
+    all_spdx_ids: List[str] = get_list_of_all_spdx_ids(document)
+    auxiliary_set = set()
+    duplicated_spdx_ids = set(
+        spdx_id for spdx_id in all_spdx_ids if spdx_id in auxiliary_set or auxiliary_set.add(spdx_id))
+
+    if duplicated_spdx_ids:
+        validation_messages.append(
+            ValidationMessage(
+                f"every spdx_id must be unique within the document, but found the following duplicates: {sorted(duplicated_spdx_ids)}",
+                context)
+        )
+
     return validation_messages
diff --git a/src/spdx/validation/spdx_id_validators.py b/src/spdx/validation/spdx_id_validators.py
index dfe25f4..cdf8f0c 100644
--- a/src/spdx/validation/spdx_id_validators.py
+++ b/src/spdx/validation/spdx_id_validators.py
@@ -53,8 +53,7 @@ def validate_spdx_id(spdx_id: str, document: Document, check_document: bool = Fa
     str]:
     """ Test that the given spdx_id (and a potential DocumentRef to an external document) is valid
     and, if it is a reference, actually exists in the document. Optionally checks files or the whole document
-    for the existence of the spdx_id (i.e. if it is used as a reference). Returns a list of validation messages,
-    and the external document ref part and id part of the provided spdx_id. """
+    for the existence of the spdx_id (i.e. if it is used as a reference). Returns a list of validation messages. """
 
     validation_messages: List[str] = []
     split_id: List[str] = spdx_id.split(":")
@@ -74,7 +73,7 @@ def validate_spdx_id(spdx_id: str, document: Document, check_document: bool = Fa
                 f'the internal SPDX id part of spdx_id must only contain letters, numbers, "." and "-" and must begin with "SPDXRef-", but is: {split_id[1]}')
         if not is_external_doc_ref_present_in_document(split_id[0], document):
             validation_messages.append(
-                f"did not find the external document reference {split_id[0]} in the SPDX document")
+                f'did not find the external document reference "{split_id[0]}" in the SPDX document')
 
         return validation_messages
 
@@ -85,10 +84,10 @@ def validate_spdx_id(spdx_id: str, document: Document, check_document: bool = Fa
 
     if check_document:
         if not is_spdx_id_present_in_document(spdx_id, document):
-            validation_messages.append(f"did not find the referenced spdx_id {spdx_id} in the SPDX document")
+            validation_messages.append(f'did not find the referenced spdx_id "{spdx_id}" in the SPDX document')
 
     if check_files:
         if not is_spdx_id_present_in_files(spdx_id, document.files):
-            validation_messages.append(f"did not find the referenced spdx_id {spdx_id} in the SPDX document's files")
+            validation_messages.append(f'did not find the referenced spdx_id "{spdx_id}" in the SPDX document\'s files')
 
     return validation_messages


Test patch:
diff --git a/tests/spdx/validation/test_annotation_validator.py b/tests/spdx/validation/test_annotation_validator.py
index b37a686..608de5b 100644
--- a/tests/spdx/validation/test_annotation_validator.py
+++ b/tests/spdx/validation/test_annotation_validator.py
@@ -28,7 +28,7 @@ def test_valid_annotation():
 
 @pytest.mark.parametrize("annotation_id, file_id, expected_message",
                          [("SPDXRef-File", "SPDXRef-hiddenFile",
-                           "did not find the referenced spdx_id SPDXRef-File in the SPDX document")
+                           'did not find the referenced spdx_id "SPDXRef-File" in the SPDX document')
                           ])
 def test_invalid_annotation(annotation_id, file_id, expected_message):
     annotation: Annotation = annotation_fixture(spdx_id=annotation_id)
diff --git a/tests/spdx/validation/test_document_validator.py b/tests/spdx/validation/test_document_validator.py
index c3b8055..944f307 100644
--- a/tests/spdx/validation/test_document_validator.py
+++ b/tests/spdx/validation/test_document_validator.py
@@ -16,7 +16,7 @@ import pytest
 from spdx.model.document import Document, CreationInfo
 from spdx.validation.document_validator import validate_full_spdx_document
 from spdx.validation.validation_message import ValidationMessage, ValidationContext, SpdxElementType
-from tests.spdx.fixtures import document_fixture, creation_info_fixture
+from tests.spdx.fixtures import document_fixture, creation_info_fixture, file_fixture, package_fixture, snippet_fixture
 
 
 def test_valid_document():
@@ -56,3 +56,18 @@ def test_spdx_version_handling(creation_info: CreationInfo, version_input: str,
     assert validation_messages == expected
 
     # TODO: https://github.com/spdx/tools-python/issues/375
+
+
+def test_duplicated_spdx_ids():
+    document = document_fixture(
+        files=[file_fixture(spdx_id="SPDXRef-File"), file_fixture(spdx_id="SPDXRef-2"), file_fixture(spdx_id="SPDXRef-3")],
+        packages=[package_fixture(spdx_id="SPDXRef-2"), package_fixture(spdx_id="SPDXRef-DOCUMENT")],
+        snippets=[snippet_fixture(spdx_id="SPDXRef-2"), snippet_fixture(spdx_id="SPDXRef-3")])
+
+    context = ValidationContext(spdx_id=document.creation_info.spdx_id, element_type=SpdxElementType.DOCUMENT)
+
+    validation_messages: List[ValidationMessage] = validate_full_spdx_document(document)
+
+    assert validation_messages == [ValidationMessage(
+        "every spdx_id must be unique within the document, but found the following duplicates: ['SPDXRef-2', 'SPDXRef-3', 'SPDXRef-DOCUMENT']",
+        context)]
diff --git a/tests/spdx/validation/test_relationship_validator.py b/tests/spdx/validation/test_relationship_validator.py
index c93c2e2..89ea088 100644
--- a/tests/spdx/validation/test_relationship_validator.py
+++ b/tests/spdx/validation/test_relationship_validator.py
@@ -33,9 +33,9 @@ def test_valid_relationship(related_spdx_element):
 
 @pytest.mark.parametrize("spdx_element_id, related_spdx_element_id, expected_message",
                          [("SPDXRef-unknownFile", "SPDXRef-File",
-                           'did not find the referenced spdx_id SPDXRef-unknownFile in the SPDX document'),
+                           'did not find the referenced spdx_id "SPDXRef-unknownFile" in the SPDX document'),
                           ("SPDXRef-File", "SPDXRef-unknownFile",
-                           'did not find the referenced spdx_id SPDXRef-unknownFile in the SPDX document'),
+                           'did not find the referenced spdx_id "SPDXRef-unknownFile" in the SPDX document'),
                           ])
 def test_unknown_spdx_id(spdx_element_id, related_spdx_element_id, expected_message):
     relationship: Relationship = relationship_fixture(spdx_element_id=spdx_element_id,
diff --git a/tests/spdx/validation/test_spdx_id_validators.py b/tests/spdx/validation/test_spdx_id_validators.py
index 4c00169..ee8536c 100644
--- a/tests/spdx/validation/test_spdx_id_validators.py
+++ b/tests/spdx/validation/test_spdx_id_validators.py
@@ -8,5 +8,115 @@
 # 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 TestCase
+
+import pytest
+
+from spdx.validation.spdx_id_validators import is_valid_internal_spdx_id, is_valid_external_doc_ref_id, \
+    get_list_of_all_spdx_ids, is_spdx_id_present_in_document, is_external_doc_ref_present_in_document, validate_spdx_id
+from tests.spdx.fixtures import document_fixture, file_fixture, package_fixture, snippet_fixture, creation_info_fixture, \
+    external_document_ref_fixture
+
+DOCUMENT = document_fixture(files=[file_fixture(spdx_id="SPDXRef-File1"),
+                                   file_fixture(spdx_id="SPDXRef-File2")],
+                            packages=[package_fixture(spdx_id="SPDXRef-Package1"),
+                                      package_fixture(spdx_id="SPDXRef-Package2")],
+                            snippets=[snippet_fixture(spdx_id="SPDXRef-Snippet1"),
+                                      snippet_fixture(spdx_id="SPDXRef-Snippet2")],
+                            creation_info=creation_info_fixture(
+                                external_document_refs=[external_document_ref_fixture(document_ref_id="DocumentRef-external"),
+                                                        external_document_ref_fixture(document_ref_id="DocumentRef-1.2-ext")]))
+
+
+@pytest.mark.parametrize("spdx_id", ["SPDXRef-DOCUMENT", "SPDXRef-File1", "SPDXRef-1.3-3.7"])
+def test_valid_internal_spdx_ids(spdx_id):
+    assert is_valid_internal_spdx_id(spdx_id)
+
+
+@pytest.mark.parametrize("spdx_id",
+                         ["spdxId", "spdxRef-DOCUMENT", "SPDXRef.File", "SPDXRef#Snippet", "SPDXRef-1.3_3.7"])
+def test_invalid_internal_spdx_ids(spdx_id):
+    assert not is_valid_internal_spdx_id(spdx_id)
+
+
+@pytest.mark.parametrize("doc_ref_id", ["DocumentRef-external", "DocumentRef-...+", "DocumentRef-v0.4.2-alpha"])
+def test_valid_external_doc_ref_ids(doc_ref_id):
+    assert is_valid_external_doc_ref_id(doc_ref_id)
+
+
+@pytest.mark.parametrize("doc_ref_id",
+                         ["external-ref", "Documentref-external", "DocumentRef-...#", "DocumentRef-v0_4_2-alpha"])
+def test_invalid_external_doc_ref_ids(doc_ref_id):
+    assert not is_valid_external_doc_ref_id(doc_ref_id)
+
+
+def test_is_spdx_id_present_in_document():
+    assert is_spdx_id_present_in_document("SPDXRef-File1", DOCUMENT)
+    assert is_spdx_id_present_in_document("SPDXRef-Package2", DOCUMENT)
+    assert is_spdx_id_present_in_document("SPDXRef-Snippet1", DOCUMENT)
+    assert is_spdx_id_present_in_document("SPDXRef-DOCUMENT", DOCUMENT)
+    assert not is_spdx_id_present_in_document("SPDXRef-file2", DOCUMENT)
+
+
+def test_is_external_doc_ref_present_in_document():
+    assert is_external_doc_ref_present_in_document("DocumentRef-1.2-ext", DOCUMENT)
+    assert not is_external_doc_ref_present_in_document("DocumentRef-External1", DOCUMENT)
+
+def test_list_of_all_spdx_ids():
+    TestCase().assertCountEqual(get_list_of_all_spdx_ids(DOCUMENT),
+                                ["SPDXRef-DOCUMENT", "SPDXRef-File1", "SPDXRef-File2", "SPDXRef-Package1",
+                                 "SPDXRef-Package2", "SPDXRef-Snippet1", "SPDXRef-Snippet2"])
+
+
+@pytest.mark.parametrize("spdx_id",
+                         ["DocumentRef-external:SPDXRef-File", "SPDXRef-Package"])
+def test_valid_spdx_id(spdx_id):
+    validation_messages = validate_spdx_id(spdx_id, DOCUMENT)
+
+    assert validation_messages == []
+
+
+@pytest.mark.parametrize("spdx_id, expected_messages",
+                         [("DocumentRef-external:extern:SPDXRef-File",
+                           [f"spdx_id must not contain more than one colon in order to separate the external document reference id from the internal SPDX id, but is: DocumentRef-external:extern:SPDXRef-File"]),
+                          ("DocumentRef external:SPDXRef-File",
+                           ['the external document reference part of spdx_id must only contain letters, numbers, ".", "-" and "+" and must begin with "DocumentRef-", but is: DocumentRef external',
+                            'did not find the external document reference "DocumentRef external" in the SPDX document']),
+                          ("DocRef-ext:SPDXRef-File_2",
+                           ['the external document reference part of spdx_id must only contain letters, numbers, ".", "-" and "+" and must begin with "DocumentRef-", but is: DocRef-ext',
+                            'the internal SPDX id part of spdx_id must only contain letters, numbers, "." and "-" and must begin with "SPDXRef-", but is: SPDXRef-File_2',
+                            'did not find the external document reference "DocRef-ext" in the SPDX document']),
+                          ("DocumentRef-external:SPDXRef-File_2",
+                           ['the internal SPDX id part of spdx_id must only contain letters, numbers, "." and "-" and must begin with "SPDXRef-", but is: SPDXRef-File_2']),
+                          ("SPDXRef-42+",
+                           ['spdx_id must only contain letters, numbers, "." and "-" and must begin with "SPDXRef-", but is: SPDXRef-42+'])
+                          ])
+def test_invalid_spdx_id(spdx_id, expected_messages):
+    validation_messages = validate_spdx_id(spdx_id, DOCUMENT)
+
+    TestCase().assertCountEqual(validation_messages, expected_messages)
+
+
+@pytest.mark.parametrize("spdx_id",
+                         ["DocumentRef-external:SPDXRef-File", "SPDXRef-DOCUMENT", "SPDXRef-File1", "SPDXRef-Package1", "SPDXRef-Snippet1"])
+def test_valid_spdx_id_with_check_document(spdx_id):
+    validation_messages = validate_spdx_id(spdx_id, DOCUMENT, check_document=True)
+    assert validation_messages == []
+
+
+def test_invalid_spdx_id_with_check_document():
+    validation_messages = validate_spdx_id("SPDXRef-Filet", DOCUMENT, check_document=True)
+    assert validation_messages == ['did not find the referenced spdx_id "SPDXRef-Filet" in the SPDX document']
+
+
+@pytest.mark.parametrize("spdx_id",
+                         ["DocumentRef-external:SPDXRef-File", "SPDXRef-File1"])
+def test_valid_spdx_id_with_check_files(spdx_id):
+    validation_messages = validate_spdx_id(spdx_id, DOCUMENT, check_files=True)
+    assert validation_messages == []
+
+
+def test_invalid_spdx_id_with_check_files():
+    validation_messages = validate_spdx_id("SPDXRef-Package1", DOCUMENT, check_files=True)
+    assert validation_messages == ['did not find the referenced spdx_id "SPDXRef-Package1" in the SPDX document\'s files']
 
-# TODO: https://github.com/spdx/tools-python/issues/376