You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
depot_tools/metadata/tests/dependency_metadata_test.py

459 lines
22 KiB
Python

#!/usr/bin/env vpython3
# Copyright (c) 2023 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import os
import sys
import unittest
_THIS_DIR = os.path.abspath(os.path.dirname(__file__))
# The repo's root directory.
_ROOT_DIR = os.path.abspath(os.path.join(_THIS_DIR, "..", ".."))
# Add the repo's root directory for clearer imports.
sys.path.insert(0, _ROOT_DIR)
import metadata.dependency_metadata as dm
import metadata.fields.known as known_fields
import metadata.validation_result as vr
class DependencyValidationTest(unittest.TestCase):
def test_repeated_field(self):
"""Check that a validation error is returned for a repeated
field.
"""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.NAME.get_name(),
"Test repeated field")
dependency.add_entry(known_fields.URL.get_name(),
"https://www.example.com")
dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no")
dependency.add_entry(known_fields.SHIPPED.get_name(), "no")
dependency.add_entry(known_fields.NAME.get_name(), "again")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
self.assertEqual(len(results), 1)
self.assertTrue(isinstance(results[0], vr.ValidationError))
self.assertEqual(results[0].get_reason(), "There is a repeated field.")
def test_only_alias_field(self):
"""Check that an alias field can be used for a main field."""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.URL.get_name(),
"https://www.example.com")
dependency.add_entry(known_fields.NAME.get_name(),
"Test alias field used")
dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
# Use Shipped in Chromium instead of Shipped.
dependency.add_entry(known_fields.SHIPPED_IN_CHROMIUM.get_name(), "no")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
self.assertEqual(len(results), 0)
def test_alias_overwrite_invalid_field(self):
"""Check that the value for an overwritten field (from an alias
field) is still validated.
"""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.URL.get_name(),
"https://www.example.com")
dependency.add_entry(known_fields.NAME.get_name(),
"Test alias field overwrite")
dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.SHIPPED_IN_CHROMIUM.get_name(), "no")
dependency.add_entry(known_fields.SHIPPED.get_name(), "test")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
self.assertEqual(len(results), 1)
self.assertEqual(results[0].get_reason(), "Shipped is invalid.")
def test_alias_invalid_field_attributed(self):
"""Check that an invalid value from an alias field is attributed
to that alias field.
"""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.URL.get_name(),
"https://www.example.com")
dependency.add_entry(known_fields.NAME.get_name(),
"Test alias field error attributed")
dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.SHIPPED_IN_CHROMIUM.get_name(),
"test")
dependency.add_entry(known_fields.SHIPPED.get_name(), "yes")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
self.assertEqual(len(results), 1)
self.assertEqual(results[0].get_reason(),
"Shipped in Chromium is invalid.")
def test_versioning_field(self):
"""Check that a validation error is returned for insufficient
versioning info. No Date/Revision and Version is N/A."""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.NAME.get_name(),
"Test metadata missing versioning info")
dependency.add_entry(known_fields.URL.get_name(),
"https://www.example.com")
dependency.add_entry(known_fields.VERSION.get_name(), "N/A")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no")
dependency.add_entry(known_fields.SHIPPED.get_name(), "no")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
self.assertEqual(len(results), 1)
self.assertTrue(isinstance(results[0], vr.ValidationError))
self.assertEqual(results[0].get_reason(),
"Versioning fields are insufficient.")
def test_versioning_with_invalid_revision(self):
"""Check that a validation error is returned for insufficient
versioning info."""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.NAME.get_name(),
"Test metadata missing versioning info")
dependency.add_entry(known_fields.URL.get_name(),
"https://www.example.com")
dependency.add_entry(known_fields.VERSION.get_name(), "N/A")
dependency.add_entry(known_fields.REVISION.get_name(), "N/A")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no")
dependency.add_entry(known_fields.SHIPPED.get_name(), "no")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
self.assertEqual(len(results), 2)
self.assertTrue(isinstance(results[0], vr.ValidationError))
self.assertTrue(isinstance(results[1], vr.ValidationError))
self.assertEqual(results[0].get_reason(),
"Revision is not a valid hexadecimal revision.")
self.assertEqual(results[1].get_reason(),
"Versioning fields are insufficient.")
def test_invalid_revision(self):
"""Check invalid revision formats return validation errors."""
# Test invalid revision format (non-hexadecimal).
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.NAME.get_name(), "Invalid Revision")
dependency.add_entry(known_fields.URL.get_name(),
"https://www.example.com")
dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0")
dependency.add_entry(known_fields.REVISION.get_name(),
"invalid_revision")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no")
dependency.add_entry(known_fields.SHIPPED.get_name(), "no")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
self.assertEqual(len(results), 1)
self.assertTrue(isinstance(results[0], vr.ValidationError))
self.assertEqual(
results[0].get_reason(),
"Revision is not a valid hexadecimal revision.",
)
def test_valid_revision(self):
"""Check valid revision formats return no validation issues."""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.NAME.get_name(), "Valid Revision")
dependency.add_entry(known_fields.URL.get_name(),
"https://www.example.com")
dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no")
dependency.add_entry(known_fields.SHIPPED.get_name(), "no")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
# No errors for no revision.
self.assertEqual(len(results), 0)
dependency.add_entry(known_fields.REVISION.get_name(),
"abcdef1") # Valid.
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
# No errors for valid revision.
self.assertEqual(len(results), 0)
def test_valid_revision_in_deps(self):
"""Check "Revision: DEPS" is acceptable."""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.NAME.get_name(), "Dependency")
dependency.add_entry(known_fields.URL.get_name(),
"https://www.example.com")
dependency.add_entry(known_fields.VERSION.get_name(), "N/A")
dependency.add_entry(known_fields.REVISION.get_name(), "DEPS")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no")
dependency.add_entry(known_fields.SHIPPED.get_name(), "no")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
# No errors for no revision.
self.assertEqual(len(results), 0)
def test_required_field(self):
"""Check that a validation error is returned for a missing field."""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.SHIPPED.get_name(), "no")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0")
dependency.add_entry(known_fields.NAME.get_name(), "Test missing field")
# Leave URL field unspecified.
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
self.assertEqual(len(results), 1)
self.assertTrue(isinstance(results[0], vr.ValidationError))
self.assertEqual(results[0].get_reason(),
"Required field 'URL' is missing.")
def test_invalid_field(self):
"""Check field validation issues are returned."""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.URL.get_name(),
"https://www.example.com")
dependency.add_entry(known_fields.NAME.get_name(), "Test invalid field")
dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.SHIPPED.get_name(), "no")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "test")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
self.assertEqual(len(results), 1)
self.assertTrue(isinstance(results[0], vr.ValidationError))
self.assertEqual(results[0].get_reason(),
"Security Critical is invalid.")
def test_invalid_license_file_path(self):
"""Check license file path validation issues are returned."""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.NAME.get_name(),
"Test license file path")
dependency.add_entry(known_fields.URL.get_name(),
"https://www.example.com")
dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(),
"MISSING-LICENSE")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no")
dependency.add_entry(known_fields.SHIPPED.get_name(), "no")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
self.assertEqual(len(results), 1)
self.assertTrue(isinstance(results[0], vr.ValidationWarning))
self.assertEqual(results[0].get_reason(), "License File is invalid.")
def test_multiple_validation_issues(self):
"""Check all validation issues are returned."""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.NAME.get_name(),
"Test multiple errors")
# Leave URL field unspecified.
dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(),
"MISSING-LICENSE")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "test")
dependency.add_entry(known_fields.SHIPPED.get_name(), "no")
dependency.add_entry(known_fields.NAME.get_name(), "again")
# Check 4 validation results are returned, for:
# - missing field;
# - invalid license file path;
# - invalid yes/no field value; and
# - repeated field entry.
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
self.assertEqual(len(results), 4)
def test_valid_metadata(self):
"""Check valid metadata returns no validation issues."""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.NAME.get_name(),
"Test valid metadata")
dependency.add_entry(known_fields.URL.get_name(),
"https://www.example.com")
dependency.add_entry(known_fields.VERSION.get_name(), "1.0.0")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "no")
dependency.add_entry(known_fields.SHIPPED.get_name(), "no")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
self.assertEqual(len(results), 0)
def test_dep_is_canonical_skips_versioning_requirement(self):
"""
Check versioning information isn't required for dependencies where
Chromium is the canonical repository.
"""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.NAME.get_name(),
"Test valid metadata")
dependency.add_entry(known_fields.URL.get_name(),
"This is the canonical repository")
dependency.add_entry(known_fields.VERSION.get_name(), "N/A")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "yes")
dependency.add_entry(known_fields.SHIPPED.get_name(), "yes")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
self.assertEqual(len(results), 0)
def test_all_licenses_allowlisted(self):
"""Test that a single allowlisted license returns True."""
dependency = dm.DependencyMetadata()
self.assertTrue(dependency.all_licenses_allowlisted("MIT", False))
self.assertTrue(dependency.all_licenses_allowlisted("MIT, GPL-2.0", False))
self.assertTrue(dependency.all_licenses_allowlisted("MIT, Apache-2.0", False))
self.assertFalse(dependency.all_licenses_allowlisted("InvalidLicense", False))
self.assertFalse(dependency.all_licenses_allowlisted("MIT, InvalidLicense", False))
self.assertFalse(dependency.all_licenses_allowlisted("", False))
# "MPL-2.0" is a reciprocal license, i.e. only allowed in open source projects.
self.assertTrue(dependency.all_licenses_allowlisted("MPL-2.0", True))
self.assertFalse(dependency.all_licenses_allowlisted("MPL-2.0", False))
# Restricted licenses are treated the same as other license types, until
# the exception and enforcement is resourced.
self.assertTrue(dependency.all_licenses_allowlisted("GPL-2.0", False))
self.assertTrue(dependency.all_licenses_allowlisted("GPL-2.0", True))
self.assertFalse(dependency.all_licenses_allowlisted("MPL-2.0, GPL-2.0", False))
def test_only_open_source_licenses(self):
"""Test that only open source licenses are returned."""
dependency = dm.DependencyMetadata()
self.assertEqual(dependency.only_open_source_licenses(""), [])
self.assertEqual(dependency.only_open_source_licenses("MIT"), [])
self.assertEqual(dependency.only_open_source_licenses("GPL-2.0"), [])
self.assertEqual(dependency.only_open_source_licenses("MPL-2.0"), ["MPL-2.0"])
result = dependency.only_open_source_licenses("MIT, MPL-2.0")
self.assertEqual(result, ["MPL-2.0"])
result = dependency.only_open_source_licenses("MPL-2.0, APSL-2.0")
self.assertEqual(set(result), {"MPL-2.0", "APSL-2.0"})
# Test with mix of invalid and valid licenses
result = dependency.only_open_source_licenses("InvalidLicense, MPL-2.0")
self.assertEqual(result, ["MPL-2.0"])
def test_mitigated_validation(self):
"""Tests validation of Mitigated field and corresponding CVE descriptions."""
dependency = dm.DependencyMetadata()
dependency.add_entry(known_fields.NAME.get_name(), "Test Dependency")
dependency.add_entry(known_fields.URL.get_name(), "http://example.com")
dependency.add_entry(known_fields.VERSION.get_name(), "1.0")
dependency.add_entry(known_fields.LICENSE.get_name(), "MIT")
dependency.add_entry(known_fields.LICENSE_FILE.get_name(), "LICENSE")
dependency.add_entry(known_fields.SECURITY_CRITICAL.get_name(), "yes")
dependency.add_entry(known_fields.SHIPPED.get_name(), "yes")
# Add description for one CVE and an extra one.
dependency.add_entry("CVE-2024-1234", "Fixed in this version")
dependency.add_entry("CVE-2024-9999", "This shouldn't be here")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
# Check that a warning is returned when only CVE descriptions are
# present.
self.assertEqual(len(results), 1)
self.assertTrue(isinstance(results[0], vr.ValidationWarning))
self.assertEqual(results[0].get_reason(),
"Found descriptions for unlisted vulnerability IDs")
self.assertIn("CVE-2024-1234",results[0].get_additional()[0])
self.assertIn("CVE-2024-9999",results[0].get_additional()[0])
# Add Mitigated field with two CVEs.
dependency.add_entry(known_fields.MITIGATED.get_name(),
"CVE-2024-1234, CVE-2024-5678")
results = dependency.validate(
source_file_dir=os.path.join(_THIS_DIR, "data"),
repo_root_dir=_THIS_DIR,
)
# Should get two warnings:
# 1. Missing description for CVE-2024-5678
# 2. Extra description for CVE-2024-9999
self.assertEqual(len(results), 2)
self.assertTrue(isinstance(results[0], vr.ValidationWarning))
self.assertEqual(results[0].get_reason(),
"Missing descriptions for vulnerability IDs")
self.assertIn("CVE-2024-5678",results[0].get_additional()[0])
self.assertTrue(isinstance(results[1], vr.ValidationWarning))
self.assertEqual(results[1].get_reason(),
"Found descriptions for unlisted vulnerability IDs")
self.assertIn("CVE-2024-9999",results[1].get_additional()[0])
if __name__ == "__main__":
unittest.main()