Files
scoop/tests/test_update_manifests.py
T

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()