mirror of https://github.com/mongodb/mongo
225 lines
9.0 KiB
Python
225 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests for buildscripts/sbom/*.py
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import sys
|
|
import unittest
|
|
|
|
sys.path.append("buildscripts/sbom")
|
|
|
|
from buildscripts.sbom.config import get_semver_from_release_version, regex_semver
|
|
from buildscripts.sbom.endorctl_utils import EndorCtl
|
|
from buildscripts.sbom.generate_sbom import is_valid_purl
|
|
|
|
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
|
|
|
|
|
|
class TestEndorctl(unittest.TestCase):
|
|
def test_endorctl_init(self):
|
|
"""Tests the Endorctl constructor."""
|
|
e = EndorCtl(namespace="mongodb.10gen", retry_limit=1, sleep_duration=5)
|
|
self.assertEqual(e.namespace, "mongodb.10gen")
|
|
self.assertEqual(e.retry_limit, 1)
|
|
self.assertEqual(e.sleep_duration, 5)
|
|
|
|
def test_call_endorctl_missing(self):
|
|
"""Tests EndorCtl execution with endorctl not in path."""
|
|
logger = logging.getLogger("generate_sbom")
|
|
logger.setLevel(logging.INFO)
|
|
|
|
e = EndorCtl(namespace="mongodb.10gen", endorctl_path="this_path_does_not_exist")
|
|
result = e.get_sbom_for_project("https://github.com/10gen/mongo.git")
|
|
self.assertRaises(FileNotFoundError)
|
|
self.assertIsNone(result, None)
|
|
|
|
|
|
class TestConfigRegex(unittest.TestCase):
|
|
def test_semver_regex(self):
|
|
"""Tests the regex_semver."""
|
|
|
|
# List of valid semantic version strings
|
|
valid_semvers = [
|
|
"0.0.1",
|
|
"1.2.3",
|
|
"10.20.30",
|
|
"1.2.3-alpha",
|
|
"1.2.3-alpha.1",
|
|
"1.2.3-0.beta",
|
|
"1.2.3+build.123",
|
|
"1.2.3-rc.1+build.456",
|
|
"1.0.0-beta+exp.sha.5114f85",
|
|
]
|
|
|
|
# List of invalid semantic version strings
|
|
invalid_semvers = [
|
|
"1.2", # Incomplete
|
|
"1", # Incomplete
|
|
"v1.2.3", # Has a 'v' prefix (regex is for the version part only)
|
|
"1.2.3-", # Trailing hyphen in pre-release
|
|
"1.2.3+", # Trailing plus in build
|
|
"1.02.3", # Leading zero in minor component
|
|
"1.2.03", # Leading zero in patch component
|
|
"alpha", # Not a valid version
|
|
"1.2.3.4", # Four components (SemVer is 3)
|
|
"1.2.3-alpha_beta", # Underscore in pre-release
|
|
]
|
|
|
|
print("\nTesting regex_semver:")
|
|
for v in valid_semvers:
|
|
with self.subTest(v=v):
|
|
self.assertIsNotNone(
|
|
regex_semver.fullmatch(v), f"Expected '{v}' to be a valid semver"
|
|
)
|
|
|
|
for v in invalid_semvers:
|
|
with self.subTest(v=v):
|
|
self.assertIsNone(
|
|
regex_semver.fullmatch(v), f"Expected '{v}' to be an invalid semver"
|
|
)
|
|
|
|
def test_get_semver_from_release_version(self):
|
|
"""Tests the transformation function that uses VERSION_PATTERN_REPL."""
|
|
|
|
# (input, expected_output)
|
|
test_cases = [
|
|
# Pattern 1: 'debian/1.28.1-1'
|
|
("debian/1.28.1-1", "1.28.1"),
|
|
("debian/1.2.3-rc.1-2", "1.2.3-rc.1"),
|
|
# Pattern 2: 'gperftools-2.9.1', 'mongo/v1.5.2', etc.
|
|
("gperftools-2.9.1", "2.9.1"),
|
|
("mongo/v1.5.2", "1.5.2"),
|
|
("mongodb-8.2.0-alpha2", "8.2.0-alpha2"),
|
|
("release-1.12.0", "1.12.0"),
|
|
("yaml-cpp-0.6.3", "0.6.3"),
|
|
("mongo/1.2.3-beta+build", "1.2.3-beta+build"),
|
|
# Pattern 3: 'asio-1-34-2', 'cares-1_27_0'
|
|
("asio-1-34-2", "1.34.2"),
|
|
("cares-1_27_0", "1.27.0"),
|
|
# Pattern 4: 'pcre2-10.40'
|
|
("pcre2-10.40", "10.40"),
|
|
("something-1.2", "1.2"),
|
|
# Pattern 5: 'icu-release-57-1'
|
|
("icu-release-57-1", "57.1"),
|
|
("foo-bar-12-3", "12.3"),
|
|
# Pattern 6: 'v2.6.0'
|
|
("v2.6.0", "2.6.0"),
|
|
("v1.2.3-alpha.1", "1.2.3-alpha.1"),
|
|
# Pattern 7: 'r2.5.1'
|
|
("r2.5.1", "2.5.1"),
|
|
("r1.2.3-alpha.1", "1.2.3-alpha.1"),
|
|
# Pattern 7: 'v2025.04.21.00' (non-semver but specific pattern)
|
|
("v2025.04.21.00", "2025.04.21.00"),
|
|
# --- Cases that should not match ---
|
|
("1.2.3", "1.2.3"), # Already clean
|
|
("latest", "latest"), # No match
|
|
("not-a-version", "not-a-version"), # No match
|
|
("v1.2", "v1.2"), # Not matched by any pattern
|
|
]
|
|
|
|
print("\nTesting get_semver_from_release_version():")
|
|
for input_str, expected_str in test_cases:
|
|
with self.subTest(input=input_str):
|
|
result = get_semver_from_release_version(input_str)
|
|
self.assertEqual(
|
|
result,
|
|
expected_str,
|
|
f"Input: '{input_str}', Expected: '{expected_str}', Got: '{result}'",
|
|
)
|
|
|
|
def test_purls_valid(self):
|
|
"""Tests valid PURLs."""
|
|
valid_purls = [
|
|
"pkg:github/gperftools/gperftools@gperftools-2.9.1",
|
|
"pkg:github/mongodb/mongo-c-driver@1.23.4",
|
|
"pkg:github/google/benchmark", # No version
|
|
"pkg:github/c-ares/c-ares@cares-1_27_0",
|
|
"pkg:github/apache/avro@release-1.12.0",
|
|
"pkg:github/jbeder/yaml-cpp@yaml-cpp-0.6.3",
|
|
"pkg:github/pcre2project/pcre2@pcre2-10.40",
|
|
"pkg:github/unicode-org/icu@icu-release-57-1",
|
|
"pkg:github/confluentinc/librdkafka@v2.6.0",
|
|
"pkg:github/facebook/folly@v2025.04.21.00?foo=bar#src/main", # With qualifiers/subpath
|
|
"pkg:generic/valgrind/valgrind@3.23.0", # namespace/name@version
|
|
"pkg:generic/intel/IntelRDFPMathLib@2.0u2",
|
|
"pkg:generic/openldap/openldap", # namespace/name
|
|
"pkg:generic/openssl@3.0.13", # name@version
|
|
"pkg:generic/my-package", # name only
|
|
"pkg:generic/my-package@1.2.3?arch=x86_64#README.md", # With qualifiers/subpath
|
|
"pkg:deb/debian/firefox-esr@128.11.0esr-1?arch=source",
|
|
"pkg:pypi/ocspbuilder@0.10.2",
|
|
]
|
|
|
|
print("\nTesting Valid PURLs:")
|
|
for purl in valid_purls:
|
|
with self.subTest(purl=purl):
|
|
self.assertTrue(is_valid_purl(purl), f"Expected '{purl}' to be valid")
|
|
|
|
def test_purls_invalid(self):
|
|
"""Tests invalid PURLs."""
|
|
invalid_purls = [
|
|
"pkg:github/gperftools", # Missing name
|
|
"pkg:github/", # Missing namespace and name
|
|
"pkg:c/github.com/abseil/abseil-cpp", # Wrong type (from your config.py)
|
|
"pkg:github/mongodb/mongo-c-driver@1.2.3@4.5.6", # Double version
|
|
"pkg:generic/github/mongodb/mongo", # Wrong type
|
|
"pkg:generic/", # Missing name
|
|
"pkg:github/valgrind/", # Missing name
|
|
"pkg:generic/my-package@1.2@3.4", # Double version
|
|
"pkg:generic/spaces in name", # Spaces not allowed (must be encoded)
|
|
"pkg:deb/firefox-esr@128.11.0esr-1?arch=source", # Missing vendor
|
|
"pkg:pypi/ocsp/ocspbuilder@0.10.2", # no namespace for PyPI
|
|
]
|
|
|
|
print("\nTesting Invalid PURLs:")
|
|
for purl in invalid_purls:
|
|
with self.subTest(purl=purl):
|
|
self.assertFalse(is_valid_purl(purl), f"Expected '{purl}' to be invalid")
|
|
|
|
|
|
__unittest = True
|
|
|
|
|
|
class TestMetadataFile(unittest.TestCase):
|
|
TEST_DIR = os.path.join("buildscripts", "sbom")
|
|
VERSION_TAG = "{{VERSION}}"
|
|
|
|
def read_sbom_json_file(self, file_path: str) -> dict:
|
|
"""Load a JSON SBOM file (schema is not validated)"""
|
|
with open(file_path, "r", encoding="utf-8") as input_json:
|
|
sbom_json = input_json.read()
|
|
return json.loads(sbom_json)
|
|
|
|
def test_metadata_sbom_version_tags(self):
|
|
sbom_metadata_file = os.path.join(self.TEST_DIR, "metadata.cdx.json")
|
|
print(sbom_metadata_file)
|
|
meta_bom = self.read_sbom_json_file(sbom_metadata_file)
|
|
for component in meta_bom["components"]:
|
|
with self.subTest(component=component):
|
|
properties = []
|
|
properties.append(component["bom-ref"])
|
|
properties.append(component["version"])
|
|
if "purl" in component:
|
|
properties.append(component["purl"])
|
|
if "cpe" in component:
|
|
properties.append(component["cpe"])
|
|
# make sure component has a minimum of bom-ref, version and at least one of purl or cpe
|
|
self.assertGreater(
|
|
len(properties),
|
|
2,
|
|
f"Component must have a minimum of bom-ref, version and at least one of purl or cpe. {properties}",
|
|
)
|
|
# make sure all properites either have version tag or no version tags
|
|
self.assertTrue(
|
|
all(self.VERSION_TAG in p for p in properties)
|
|
or all(self.VERSION_TAG not in p for p in properties),
|
|
f"Component must have version tag '{self.VERSION_TAG}' in all or none of bom-ref, version and purl and/or cpe. {properties})",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main(verbosity=2)
|