import hashlib import importlib.util import io import json from pathlib import Path import tempfile import unittest from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] spec = importlib.util.spec_from_file_location('updater', ROOT / 'scripts/update-manifests.py') updater = importlib.util.module_from_spec(spec) spec.loader.exec_module(updater) MODIFIED = 'Wed, 21 Oct 2026 06:00:00 GMT' HEADERS = {'Last-Modified': MODIFIED, 'ETag': '"example"'} def response(data=b'', headers=None): result = io.BytesIO(data) result.headers = dict(headers or HEADERS) return result class UpdaterTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.path = Path(self.temp.name) / 'myelexis-cli.json' self.manifest = json.loads((ROOT / 'bucket/myelexis-cli.json').read_text(encoding='utf-8')) self.path.write_text(json.dumps(self.manifest, indent=4) + '\n', encoding='utf-8') self.original = self.path.read_bytes() def test_same_version_only_uses_head(self): self.manifest['version'] = '20261021T060000Z' self.path.write_text(json.dumps(self.manifest), encoding='utf-8') before = self.path.read_bytes() with patch.object(updater, 'open_url', return_value=response()) as request: self.assertFalse(updater.update_manifest(self.path, write=True)) self.assertEqual(request.call_count, 1) self.assertEqual(request.call_args.args[1], 'HEAD') self.assertEqual(self.path.read_bytes(), before) def test_dry_run_downloads_and_hashes_without_writing(self): binary = b'MZexample' with patch.object(updater, 'open_url', side_effect=[response(), response(binary)]) as request: self.assertTrue(updater.update_manifest(self.path)) self.assertEqual(request.call_args.kwargs['headers']['If-Match'], HEADERS['ETag']) self.assertEqual(self.path.read_bytes(), self.original) def test_write_updates_only_version_url_and_hash(self): binary = b'MZexample' with patch.object(updater, 'open_url', side_effect=[response(), response(binary)]): updater.update_manifest(self.path, write=True) actual = json.loads(self.path.read_text()) expected = {**self.manifest, 'version': '20261021T060000Z', 'hash': hashlib.sha256(binary).hexdigest()} self.assertEqual(actual, expected) def test_download_errors_leave_manifest_unchanged(self): cases = [ response(b'MZexample', {**HEADERS, 'ETag': '"changed"'}), response(b'MZexample', {**HEADERS, 'Content-Length': '100'}), response(b'login', {**HEADERS, 'Content-Type': 'text/html'}), response(b'not-an-executable'), ] for download in cases: with self.subTest(headers=download.headers): with patch.object(updater, 'open_url', side_effect=[response(), download]): with self.assertRaises(ValueError): updater.update_manifest(self.path, write=True) self.assertEqual(self.path.read_bytes(), self.original) def test_missing_last_modified_leaves_manifest_unchanged(self): with patch.object(updater, 'open_url', return_value=response(headers={'ETag': 'x'})): with self.assertRaises(ValueError): updater.update_manifest(self.path, write=True) self.assertEqual(self.path.read_bytes(), self.original) def test_hash_runs_after_download_and_temporary_file_is_removed(self): binary = b'MZexample' temporary_paths = [] original_hash = updater.hash_file def inspect_download(path): self.assertEqual(path.read_bytes(), binary) temporary_paths.append(path) return original_hash(path) with patch.object(updater, 'open_url', return_value=response(binary)), \ patch.object(updater, 'hash_file', side_effect=inspect_download): digest, size = updater.download_hash('https://example.org/tool.exe', HEADERS) self.assertEqual(digest, hashlib.sha256(binary).hexdigest()) self.assertEqual(size, len(binary)) self.assertFalse(temporary_paths[0].parent.exists()) def test_existing_release_manifest_substitutes_version(self): manifest = { 'version': '13.6.0', 'url': 'https://example.org/releases/download/v13.6.0/tool.exe', 'hash': 'a' * 64, 'checkver': {'url': 'https://example.org/api/releases/latest', 'jsonpath': '$.tag_name', 'regex': r'^v(?[\d.]+)$'}, 'autoupdate': {'url': 'https://example.org/releases/download/v$version/tool.exe'}, } self.path.write_text(json.dumps(manifest), encoding='utf-8') binary = b'MZexample executable' with patch.object(updater, 'open_url', side_effect=[ response(b'{"tag_name":"v13.7.0"}'), response(), response(binary)]) as request: updater.update_manifest(self.path, write=True) updated = json.loads(self.path.read_text()) self.assertEqual(updated['version'], '13.7.0') self.assertIn('/v13.7.0/', updated['url']) self.assertNotIn('#', request.call_args.args[0]) self.assertEqual(updated['hash'], hashlib.sha256(binary).hexdigest()) def test_archive_filename_is_updated_but_download_url_has_no_fragment(self): manifest = json.loads((ROOT / 'bucket/elexis-3.13.json').read_text(encoding='utf-8')) self.path.write_text(json.dumps(manifest), encoding='utf-8') with patch.object(updater, 'open_url', side_effect=[response(), response(b'PKexample')]) as request: updater.update_manifest(self.path, write=True) self.assertNotIn('#', request.call_args.args[0]) self.assertTrue(json.loads(self.path.read_text())['url'].endswith('#/Elexis-20261021T060000Z.zip')) def test_http_error_is_reported_and_does_not_stop_other_manifests(self): other = self.path.with_name('other.json') other.write_text(json.dumps({'version': '1.0.0'}), encoding='utf-8') error = updater.HTTPError('https://example.org/file', 401, 'Unauthorized', {}, None) with patch.object(updater, 'open_url', side_effect=error): self.assertEqual(updater.main(['--bucket', self.temp.name, '--write']), 1) self.assertEqual(self.path.read_bytes(), self.original) if __name__ == '__main__': unittest.main()