137 lines
6.9 KiB
Python
137 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Offline provenance boundary tests for the partial Qt resource notice set."""
|
|
import json
|
|
import importlib.util
|
|
from pathlib import Path
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / 'tools'))
|
|
import record_dependency_provenance as provenance
|
|
|
|
|
|
class QtNoticeProvenanceTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temporary = tempfile.TemporaryDirectory(prefix='docview-qt-provenance-')
|
|
self.addCleanup(self.temporary.cleanup)
|
|
self.root = Path(self.temporary.name)
|
|
self.project, self.payload = self.root / 'project', self.root / 'payload'
|
|
self.original = self.project / 'resources/licenses/qt-embedded-notices'
|
|
shutil.copytree(ROOT / 'resources/licenses/qt-embedded-notices', self.original)
|
|
self.relative = 'licenses/qt6-webengine/embedded-resource-notices'
|
|
self.directory = self.payload / 'share/doc/docview/third-party' / self.relative
|
|
shutil.copytree(self.original, self.directory)
|
|
raw = (self.original / 'sources.json').read_bytes()
|
|
self.metadata = json.loads(raw)
|
|
rows = [{'path': self.relative + '/' + row['name'], 'size': row['size'], 'sha256': row['sha256'],
|
|
'origin': 'reviewed-installed-DataPack-resource-comment',
|
|
'sourceResources': row['sourceResources'], 'collectionScope': self.metadata['scope']}
|
|
for row in self.metadata['files']]
|
|
self.component = {'version': '6.11.2-1', 'source': {'status': 'not-collected'}, 'licenseTexts': rows,
|
|
'embeddedResourceNotices': {'status': 'collected-reviewed-resource-subset',
|
|
'metadataPath': self.relative + '/sources.json', 'metadataSha256': provenance.digest(raw),
|
|
'noticeCount': 7, 'sourceResourceCount': 13, 'runtimeDistribution': 'system-not-bundled',
|
|
'completeChromiumNotices': False, 'scope': self.metadata['scope']}}
|
|
self.manifest = {'host': {'qtVersion': '6.11.2'}, 'components': {'qt6-webengine': self.component},
|
|
'systemFiles': self.metadata['dataPacks']}
|
|
|
|
def record(self):
|
|
licenses = [provenance.license_evidence(self.payload, row) for row in self.component['licenseTexts']]
|
|
return provenance.qt_embedded_notice_record(self.payload, self.manifest, licenses, self.project)
|
|
|
|
def test_new_notices_metadata_inventory_and_repo_are_bound(self):
|
|
result = self.record()
|
|
self.assertEqual(result['status'], 'repository-original-and-package-matched')
|
|
self.assertEqual((result['noticeCount'], result['sourceResourceCount']), (7, 13))
|
|
self.assertIs(result['completeChromiumNotices'], False)
|
|
self.assertEqual(result['sourceCollectionStatus'], 'not-collected')
|
|
self.assertEqual(result['runtimeDistribution'], 'system-not-bundled')
|
|
self.assertEqual(result['metadata']['sha256'], result['repositoryMetadata']['sha256'])
|
|
self.assertEqual(len(result['files']), 7)
|
|
self.assertEqual(len(result['dataPacks']), 2)
|
|
|
|
def test_legacy_without_any_embedded_payload_remains_supported(self):
|
|
shutil.rmtree(self.directory)
|
|
self.component['licenseTexts'] = []
|
|
del self.component['embeddedResourceNotices']
|
|
result = self.record()
|
|
self.assertEqual(result['status'], 'not-in-this-package')
|
|
self.assertIs(result['completeChromiumNotices'], False)
|
|
|
|
def test_payload_without_manifest_metadata_is_not_legacy(self):
|
|
del self.component['embeddedResourceNotices']
|
|
with self.assertRaisesRegex(ValueError, 'lacks manifest metadata'):
|
|
self.record()
|
|
|
|
def test_changed_packaged_metadata_rejected(self):
|
|
path = self.directory / 'sources.json'
|
|
path.write_bytes(path.read_bytes() + b'\n')
|
|
with self.assertRaisesRegex(ValueError, 'metadata differs from repository'):
|
|
self.record()
|
|
|
|
def test_changed_notice_with_updated_outer_hash_still_rejected(self):
|
|
row = self.component['licenseTexts'][0]
|
|
data = b'replacement with self-consistent package inventory'
|
|
(self.payload / 'share/doc/docview/third-party' / row['path']).write_bytes(data)
|
|
row.update(size=len(data), sha256=provenance.digest(data))
|
|
with self.assertRaisesRegex(ValueError, 'differs from original'):
|
|
self.record()
|
|
|
|
def test_changed_original_notice_rejected(self):
|
|
(self.original / self.metadata['files'][0]['name']).write_bytes(b'changed repository original')
|
|
with self.assertRaisesRegex(ValueError, 'digest/size mismatch'):
|
|
self.record()
|
|
|
|
def test_changed_datapack_identity_rejected(self):
|
|
self.manifest['systemFiles'][0]['sha256'] = '0' * 64
|
|
with self.assertRaisesRegex(ValueError, 'DataPack inventory mismatch'):
|
|
self.record()
|
|
|
|
def test_omitted_or_duplicate_license_inventory_rejected(self):
|
|
original = self.component['licenseTexts'][:]
|
|
for rows in (original[:-1], original + [original[0]]):
|
|
self.component['licenseTexts'] = rows
|
|
with self.subTest(count=len(rows)), self.assertRaisesRegex(ValueError, 'license inventory differs'):
|
|
self.record()
|
|
|
|
def test_forged_resource_source_not_lost_in_generic_license_hash(self):
|
|
self.component['licenseTexts'][0]['sourceResources'][0]['resourceId'] += 1
|
|
with self.assertRaisesRegex(ValueError, 'resource provenance differs'):
|
|
self.record()
|
|
|
|
def test_manifest_cannot_claim_full_chromium_or_other_pin(self):
|
|
original = dict(self.component['embeddedResourceNotices'])
|
|
for key, value in [('completeChromiumNotices', True), ('metadataSha256', '0' * 64),
|
|
('noticeCount', 8), ('runtimeDistribution', 'bundled')]:
|
|
self.component['embeddedResourceNotices'] = {**original, key: value}
|
|
with self.subTest(key=key), self.assertRaisesRegex(ValueError, 'manifest metadata mismatch'):
|
|
self.record()
|
|
|
|
def test_partial_notices_cannot_change_source_collection_status(self):
|
|
self.component['source']['status'] = 'complete'
|
|
with self.assertRaisesRegex(ValueError, 'cannot claim complete source'):
|
|
self.record()
|
|
|
|
def test_unlisted_file_rejected(self):
|
|
(self.directory / 'extra.txt').write_bytes(b'unreviewed notice')
|
|
with self.assertRaisesRegex(ValueError, 'payload contents'):
|
|
self.record()
|
|
|
|
|
|
def load_tests(loader, tests, pattern):
|
|
# Keep the historical fixture tests intact while exposing one portable
|
|
# suite for current Qt notices and the existing provenance boundaries.
|
|
path = ROOT / 'tests/test_dependency_provenance.py'
|
|
spec = importlib.util.spec_from_file_location('docview_legacy_provenance_tests', path)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
tests.addTests(loader.loadTestsFromModule(module))
|
|
return tests
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main(verbosity=2)
|