136 lines
6.5 KiB
Python
136 lines
6.5 KiB
Python
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'<html>login</html>', {**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(?<version>[\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()
|