"""The installer bundle lock: fail-closed behaviour, and parity between the two implementations that read it. There are two verifiers on purpose - bundle-lock.ps1 runs at install time on a server where PowerShell is the only thing present, verify_bundle_lock.py runs in the Linux builder without adding pwsh as a build dependency. Two implementations of one rule drift. These tests run BOTH against the same fixtures and fail if they disagree, so the drift shows up here rather than as a bundle that one of them waves through. The PowerShell half is skipped where pwsh is absent; the Python half always runs. """ import json import re import shutil import subprocess import sys from pathlib import Path import pytest INSTALLER = Path(__file__).resolve().parents[1] / 'deploy' / 'windows' / 'installer' VERIFY_PY = INSTALLER / 'verify_bundle_lock.py' LOCK_PS1 = INSTALLER / 'bundle-lock.ps1' PWSH = shutil.which('pwsh') or shutil.which('powershell') pytestmark = pytest.mark.skipif(not VERIFY_PY.exists(), reason='installer not in this tree') def build_bundle(root): """A minimal bundle holding every REQUIRED payload directory.""" for name, files in ( ('wheels', {'alembic-1.18.4-py3-none-any.whl': b'wheel-a', 'cffi-2.1.0-cp314-cp314-win_amd64.whl': b'wheel-b'}), ('python', {'python-3.14.6-amd64.exe': b'py'}), ('httpplatformhandler', {'httpPlatformHandler_amd64.msi': b'hph'}), ): directory = root / name directory.mkdir(parents=True) for filename, content in files.items(): (directory / filename).write_bytes(content) return root def write_lock(bundle, lock_path): """Generate the lock the same way refresh-bundle-lock.ps1 does.""" sys.path.insert(0, str(INSTALLER)) import verify_bundle_lock as verifier payloads = {} for name, required, _what in verifier.PAYLOADS: directory = bundle / name if not directory.is_dir(): continue payloads[name] = {'required': required, 'files': verifier.payload_files(str(directory))} lock_path.write_text(json.dumps({ 'schema': 1, 'generated': '2026-08-03T00:00:00Z', 'pythontag': 'cp314', 'platform': 'win_amd64', 'payloads': payloads, })) def check_python(bundle, lock_path): result = subprocess.run( [sys.executable, str(VERIFY_PY), str(bundle), str(lock_path)], capture_output=True, text=True) problems = [line for line in result.stdout.splitlines() if line.strip()] # Exit status and output must agree, or a caller that checks only one of them # gets a different answer from a caller that checks the other. assert (result.returncode != 0) == bool(problems), result.stdout return problems def check_powershell(bundle, lock_path): script = ( ". '%s'; $p = Test-BundleLock -BundleRoot '%s' -Lock (Read-BundleLock '%s'); " "if ($p) { $p -join \"`n\" }" % (LOCK_PS1, bundle, lock_path)) result = subprocess.run([PWSH, '-NoProfile', '-Command', script], capture_output=True, text=True) assert result.returncode == 0, result.stderr return [line for line in result.stdout.splitlines() if line.strip()] def check_both(bundle, lock_path): """Returns the Python verdict, asserting PowerShell reports the same set. Compared as sets: both sort their output, but PowerShell's Sort-Object is culture-aware and Python's sorted() is ordinal, so the two can order the same findings differently. What must never differ is WHICH problems are found. """ problems = check_python(bundle, lock_path) if PWSH and LOCK_PS1.exists(): assert sorted(check_powershell(bundle, lock_path)) == sorted(problems), ( 'verify_bundle_lock.py and bundle-lock.ps1 disagree') return problems @pytest.fixture def locked(tmp_path): bundle = build_bundle(tmp_path / 'bundle') lock = tmp_path / 'bundle-lock.json' write_lock(bundle, lock) return bundle, lock def test_untouched_bundle_passes(locked): bundle, lock = locked assert check_both(bundle, lock) == [] def test_extra_file_fails(locked): """The gap pip's own hash checking leaves: a stale wheel nobody asked for.""" bundle, lock = locked (bundle / 'wheels' / 'stale-0.1-py3-none-any.whl').write_bytes(b'left over') problems = check_both(bundle, lock) assert any('stale-0.1' in p and 'NOT in the lock' in p for p in problems) def test_altered_file_fails(locked): bundle, lock = locked (bundle / 'wheels' / 'cffi-2.1.0-cp314-cp314-win_amd64.whl').write_bytes(b'swapped') problems = check_both(bundle, lock) assert any('does NOT match the lock' in p for p in problems) def test_missing_file_fails(locked): bundle, lock = locked (bundle / 'wheels' / 'alembic-1.18.4-py3-none-any.whl').unlink() problems = check_both(bundle, lock) assert any('missing from the bundle' in p for p in problems) def test_missing_required_payload_fails(locked): bundle, lock = locked shutil.rmtree(bundle / 'python') problems = check_both(bundle, lock) assert any(p.startswith('python/') for p in problems) def test_unlocked_payload_directory_fails(locked): """An optional payload dropped in after the lock was made is still unreviewed.""" bundle, lock = locked (bundle / 'mysql').mkdir() (bundle / 'mysql' / 'mysql-8.0.46-winx64.msi').write_bytes(b'msi') problems = check_both(bundle, lock) assert any('mysql/ is present but is not in bundle-lock.json' in p for p in problems) def test_absent_optional_payload_is_fine(locked): bundle, lock = locked assert not (bundle / 'mysql').exists() assert check_both(bundle, lock) == [] def test_missing_lock_fails_closed(locked): """No lock is a failure, not a pass. The build must not fall through to 'fine'.""" bundle, lock = locked lock.unlink() result = subprocess.run([sys.executable, str(VERIFY_PY), str(bundle), str(lock)], capture_output=True, text=True) assert result.returncode != 0 assert 'no bundle-lock.json' in result.stdout def test_malformed_lock_reports_rather_than_crashing(locked): """A truncated lock must produce a sentence, not a stack trace. shopdb-install.ps1 runs under Set-StrictMode 2.0, where reading a property that is not there throws. Both verifiers have to survive a lock missing the section they read first. """ bundle, lock = locked lock.write_text(json.dumps({'schema': 1, 'pythontag': 'cp314'})) problems = check_both(bundle, lock) assert problems == ['bundle-lock.json has no "payloads" section'] def test_lock_missing_files_section_is_not_a_silent_pass(locked): """A payload entry with no file list must not read as 'nothing expected, fine'.""" bundle, lock = locked data = json.loads(lock.read_text()) del data['payloads']['wheels']['files'] lock.write_text(json.dumps(data)) problems = check_both(bundle, lock) assert problems, 'an entry with no file list let every wheel through' assert all('wheels/' in p for p in problems) def test_payload_list_matches_powershell(): """Both files enumerate the payload directories. Same names, same required flags, same order - a directory that is required in one and optional in the other means the builder and the installer disagree about what may be absent. """ sys.path.insert(0, str(INSTALLER)) import verify_bundle_lock as verifier declared = re.findall(r"Name\s*=\s*'([a-z]+)'\s*;\s*Required\s*=\s*\$(true|false)", LOCK_PS1.read_text()) assert declared, 'could not find $script:BundlePayloads in bundle-lock.ps1' assert declared == [(name, str(required).lower()) for name, required, _ in verifier.PAYLOADS]