#!/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)