Files
SteamFramelyHomebrew--framely/tests/test_bootstrap.py
T

142 lines
7.3 KiB
Python

import hashlib
import importlib.util
import io
import json
import contextlib
import pathlib
import tarfile
import tempfile
import unittest
from types import SimpleNamespace
from unittest.mock import mock_open, patch
spec = importlib.util.spec_from_file_location('bootstrap', pathlib.Path(__file__).parents[1] / 'tools/bootstrap.py')
bootstrap = importlib.util.module_from_spec(spec)
spec.loader.exec_module(bootstrap)
class BootstrapTests(unittest.TestCase):
def test_explicit_install_and_update_skip_confirmation(self):
with tempfile.TemporaryDirectory() as tmp:
archive = pathlib.Path(tmp) / 'package.tar.gz'
checksums = pathlib.Path(tmp) / 'SHA256SUMS'
archive.touch()
checksums.touch()
with patch.object(bootstrap.os, 'uname', return_value=SimpleNamespace(machine='aarch64')), \
patch.object(bootstrap.os, 'geteuid', return_value=0), \
patch.object(bootstrap, 'confirm', side_effect=AssertionError('unexpected prompt')), \
patch.object(bootstrap, 'verify_archive'), \
patch.object(bootstrap, 'root_operation') as operation:
for action in ('install', 'update'):
bootstrap.main([action, '--archive', str(archive), '--checksums', str(checksums)])
self.assertEqual(operation.call_count, 2)
def test_github_proxy_option_reaches_engine_downloads(self):
with tempfile.TemporaryDirectory() as tmp:
archive = pathlib.Path(tmp) / 'package.tar.gz'
checksums = pathlib.Path(tmp) / 'SHA256SUMS'
archive.touch()
checksums.touch()
with patch.dict(bootstrap.os.environ, {}, clear=True), \
patch.object(bootstrap.os, 'uname', return_value=SimpleNamespace(machine='aarch64')), \
patch.object(bootstrap.os, 'geteuid', return_value=0), \
patch.object(bootstrap, 'verify_archive'), \
patch.object(bootstrap, 'root_operation'):
bootstrap.main(['install', '--archive', str(archive), '--checksums', str(checksums),
'--github-proxy', 'https://gh-proxy.com/'])
self.assertEqual(bootstrap.configured_github_proxy(), 'https://gh-proxy.com')
def test_explicit_uninstall_still_requires_confirmation(self):
with patch.object(bootstrap.os, 'uname', return_value=SimpleNamespace(machine='aarch64')), \
patch.object(bootstrap, 'confirm', return_value=False) as confirmation:
with self.assertRaisesRegex(ValueError, 'Cancelled'):
bootstrap.main(['uninstall'])
confirmation.assert_called_once()
def test_confirmation_distinguishes_no_terminal_from_decline(self):
with patch('builtins.open', side_effect=OSError('no tty')):
with self.assertRaisesRegex(ValueError, 'No interactive terminal.*--yes'):
bootstrap.confirm('install')
with patch('builtins.open', mock_open(read_data='')):
with self.assertRaisesRegex(ValueError, 'No confirmation input.*--yes'):
bootstrap.confirm('install')
with patch('builtins.open', mock_open(read_data='n\n')):
self.assertFalse(bootstrap.confirm('install'))
with patch('builtins.open', mock_open(read_data='Y\n')):
self.assertTrue(bootstrap.confirm('install'))
def test_external_checksums_match_exact_name_and_reject_tampering(self):
with tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
archive = root / 'framely-1-linux-arm64.tar.gz'
sums = root / 'SHA256SUMS'
archive.write_bytes(b'payload')
entry = hashlib.sha256(b'payload').hexdigest() + ' ' + archive.name + '\n'
sums.write_text(entry)
bootstrap.verify_archive(archive, sums)
archive.write_bytes(b'changed')
with self.assertRaises(ValueError):
bootstrap.verify_archive(archive, sums)
sums.write_text(entry * 2)
with self.assertRaises(ValueError):
bootstrap.verify_archive(archive, sums)
sums.write_text(entry.replace(archive.name, './' + archive.name))
with self.assertRaises(ValueError):
bootstrap.verify_archive(archive, sums)
def archive(self, root, entries):
archive = root / 'release.tar.gz'
with tarfile.open(archive, 'w:gz') as out:
for name, contents in entries:
member = tarfile.TarInfo(name)
if contents is None:
member.type = tarfile.SYMTYPE
member.linkname = '/etc/shadow'
out.addfile(member)
else:
member.size = len(contents)
out.addfile(member, io.BytesIO(contents))
return archive
def test_safe_extraction_checks_structure_before_writing(self):
entries = [('framely-1/' + name, b'1' if name == 'VERSION' else b'test') for name in ['VERSION', 'SHA256SUMS', 'install.sh', 'uninstall.sh', 'bin/framely']]
with tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
base = bootstrap.extract_archive(self.archive(root, entries), root / 'out')
self.assertEqual(base.name, 'framely-1')
for extra in [('framely-1/../../escape', b'bad'), ('framely-1/link', None), ('framely-1/VERSION', b'1'), ('framely-2/file', b'bad')]:
with self.subTest(extra=extra), tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
with self.assertRaises(ValueError):
bootstrap.extract_archive(self.archive(root, entries + [extra]), root / 'out')
self.assertFalse((root / 'out').exists())
def test_redirect_cannot_downgrade_https(self):
with self.assertRaises(ValueError):
bootstrap.HTTPSRedirect().redirect_request(None, None, 302, '', {}, 'http://example.org/package')
def test_extraction_progress_reports_real_bytes_and_never_completes_invalid_release(self):
entries = [('framely-1/' + name, b'1' if name == 'VERSION' else b'test') for name in ['VERSION', 'SHA256SUMS', 'install.sh', 'uninstall.sh', 'bin/framely']]
with tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
reports = []
bootstrap.extract_archive(self.archive(root, entries), root / 'out', lambda *value: reports.append(value))
total = sum(len(contents) for _, contents in entries)
self.assertEqual(reports[0], ('extract', 0, total))
self.assertEqual(reports[-1], ('extract', total, total))
with tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
reports = []
with self.assertRaises(ValueError):
bootstrap.extract_archive(self.archive(root, [('framely-1/VERSION', b'wrong')]), root / 'out', lambda *value: reports.append(value))
self.assertFalse(any(done == total and total > 0 for _, done, total in reports))
output = io.StringIO()
with contextlib.redirect_stdout(output):
bootstrap.emit_progress('configure')
payload = json.loads(output.getvalue().removeprefix('FRAMELY_PROGRESS '))
self.assertEqual(payload, {'step': 'configure'})
if __name__ == '__main__':
unittest.main()