vollständige umstellung auf gitea repo
This commit is contained in:
1 parent
3a807da54c
commit
079e69963d
9 files changed
+422
-35
No files matched your search
@@ -0,0 +1,49 @@
|
||||
name: Update Scoop manifests
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
|
||||
jobs:
|
||||
update:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
code: write
|
||||
timeout-minutes: 55
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ gitea.event.repository.default_branch }}
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check Python runtime
|
||||
run: python3 -c "import sys; assert sys.version_info >= (3, 10), 'Python 3.10+ erforderlich'"
|
||||
|
||||
- name: Test manifest updater
|
||||
run: python3 -B -m unittest discover -s tests -v
|
||||
|
||||
- name: Update manifests
|
||||
id: update
|
||||
run: python3 -B scripts/update-manifests.py --write
|
||||
|
||||
# Gültige Updates auch dann speichern, wenn ein anderer Download scheitert.
|
||||
# Der fehlgeschlagene Update-Schritt bleibt im Workflow sichtbar.
|
||||
- name: Commit and push changed manifests
|
||||
if: ${{ always() && steps.update.outcome != 'skipped' }}
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ gitea.event.repository.default_branch }}
|
||||
run: |
|
||||
set -eu
|
||||
git add -- bucket/*.json
|
||||
if git diff --cached --quiet; then
|
||||
echo 'Keine Manifest-Änderungen.'
|
||||
exit 0
|
||||
fi
|
||||
git config user.name 'Gitea Actions'
|
||||
git config user.email 'actions@gitea.medelexis.ch'
|
||||
git commit -m 'Update Scoop manifests'
|
||||
git pull --rebase origin "$DEFAULT_BRANCH"
|
||||
git push origin "HEAD:$DEFAULT_BRANCH"
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
*.bak
|
||||
*.exe
|
||||
scripts/*
|
||||
.forgejo/*
|
||||
!scripts/update-manifests.py
|
||||
.vscode/*
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
@@ -5,7 +5,7 @@
|
||||
"depends": [
|
||||
"extras/element"
|
||||
],
|
||||
"url": "https://git.streichtech.ch/elexis/scoop/raw/branch/master/config/matrix/medelexis/config.json#/config.json",
|
||||
"url": "https://gitea.medelexis.ch/public/scoop/raw/branch/master/config/matrix/medelexis/config.json#/config.json",
|
||||
"hash": "b5ed72c551ff700d86a4ae293c871528d71fa53440c46d2594916a6d1273d131",
|
||||
"post_install": [
|
||||
"$configDir = Join-Path $env:APPDATA 'Element'",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"version": "13.6.0",
|
||||
"url": "https://git.streichtech.ch/elexis/ee-iso-builder/releases/download/v13.6.0/ElexisEnvironment.iso#/ElexisEnvironment.dat",
|
||||
"hash": "58fd7e00b7f4771fd4ee2165f3ebaff205cab780461340fc2c286f1b12faae90",
|
||||
"installer": {
|
||||
"script": "Copy-Item \"$dir\\$fname\" \"$HOME\\Downloads\\ElexisEnvironment.iso\" -Force"
|
||||
},
|
||||
"post_install": [
|
||||
"$scoopCommand = (Get-Command scoop).Source",
|
||||
"$cleanupCommand = \"Start-Sleep -Seconds 10; & '$scoopCommand' cleanup ee_iso --cache; & '$scoopCommand' uninstall ee_iso --purge\"",
|
||||
"$cleanupBytes = [Text.Encoding]::Unicode.GetBytes($cleanupCommand)",
|
||||
"$encodedCommand = [Convert]::ToBase64String($cleanupBytes)",
|
||||
"Start-Process powershell.exe -WorkingDirectory $HOME -WindowStyle Hidden -ArgumentList @('-NoProfile', '-EncodedCommand', $encodedCommand)"
|
||||
],
|
||||
"checkver": {
|
||||
"url": "https://git.streichtech.ch/api/v1/repos/elexis/ee-iso-builder/releases/latest",
|
||||
"jsonpath": "$.tag_name",
|
||||
"regex": "^v(?<version>[\\d.]+)$"
|
||||
},
|
||||
"autoupdate": {
|
||||
"url": "https://git.streichtech.ch/elexis/ee-iso-builder/releases/download/v$version/ElexisEnvironment.iso#/ElexisEnvironment.dat"
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,12 @@
|
||||
"extras/keepass",
|
||||
"extras/teamviewer",
|
||||
|
||||
"ee-cli",
|
||||
"elexis_3.10",
|
||||
"elexis_3.12",
|
||||
"elexis_3.13",
|
||||
"elexis_master",
|
||||
"chat_medelexis"
|
||||
"myelexis-cli",
|
||||
"elexis-3.10",
|
||||
"elexis-3.12",
|
||||
"elexis-3.13",
|
||||
"elexis-master",
|
||||
"chat-medelexis"
|
||||
],
|
||||
"url": "https://git.streichtech.ch/elexis/scoop/raw/branch/master/packages/p.intern.txt#/p.intern.txt"
|
||||
"url": "https://gitea.medelexis.ch/public/scoop/raw/branch/master/packages/p.intern.txt#/p.intern.txt"
|
||||
}
|
||||
+2
-2
@@ -33,7 +33,7 @@ if ($bucketList -match "elexis") {
|
||||
Write-Host "elexis Bucket ist bereits vorhanden." -ForegroundColor White
|
||||
}
|
||||
else {
|
||||
scoop bucket add elexis https://git.streichtech.ch/elexis/scoop
|
||||
scoop bucket add elexis https://gitea.medelexis.ch/public/scoop
|
||||
Write-Host "elexis Bucket hinzugefügt." -ForegroundColor Green
|
||||
}
|
||||
|
||||
@@ -47,6 +47,6 @@ else {
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Fertig! Verfügbare Apps anzeigen mit: scoop search" -ForegroundColor Cyan
|
||||
Write-Host "Bsp. scoop search ee-cli" -ForegroundColor Cyan
|
||||
Write-Host "Bsp. scoop search myelexis-cli" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Read-Host "Enter drücken zum Beenden"
|
||||
@@ -0,0 +1,225 @@
|
||||
"""Update timestamp and release manifests using only Python 3.10+.
|
||||
|
||||
Without --write, calculate changes without saving them. Never commit or push.
|
||||
PowerShell statements in manifests are not executed.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from datetime import timezone
|
||||
from email.utils import parsedate_to_datetime
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urldefrag, urlsplit
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
|
||||
BUCKET = Path(__file__).resolve().parents[1] / 'bucket'
|
||||
TIMESTAMP_REGEX = r'(?<version>\d{8}T\d{6}Z)'
|
||||
|
||||
|
||||
def open_url(url, method='GET', headers=None):
|
||||
parsed = urlsplit(url)
|
||||
if parsed.scheme != 'https' or not parsed.hostname or parsed.username or parsed.password:
|
||||
raise ValueError('Nur HTTPS-URLs ohne eingebettete Zugangsdaten sind erlaubt.')
|
||||
return urlopen(Request(url, method=method, headers={
|
||||
'User-Agent': 'Medelexis-Scoop-Updater', 'Cache-Control': 'no-cache',
|
||||
'Accept-Encoding': 'identity', **(headers or {})}), timeout=180)
|
||||
|
||||
|
||||
def metadata(headers):
|
||||
return {key: headers.get(key, '') for key in ('Last-Modified', 'ETag')}
|
||||
|
||||
|
||||
def read_head(url):
|
||||
with open_url(url, 'HEAD') as response:
|
||||
return metadata(response.headers)
|
||||
|
||||
|
||||
def release_version(checkver):
|
||||
if checkver.get('jsonpath') != '$.tag_name' or not checkver.get('regex'):
|
||||
raise ValueError('Unterstützt werden Last-Modified oder jsonpath $.tag_name mit regex.')
|
||||
with open_url(checkver['url']) as response:
|
||||
tag = json.load(response)['tag_name']
|
||||
pattern = checkver['regex'].replace('(?<version>', '(?P<version>')
|
||||
match = re.search(pattern, tag)
|
||||
if not match:
|
||||
raise ValueError('Release-Tag passt nicht zum Versionsmuster.')
|
||||
version = match.groupdict().get('version') or match.group(1)
|
||||
if not re.fullmatch(r'[0-9A-Za-z][0-9A-Za-z._+-]*', version):
|
||||
raise ValueError('Ungültige Release-Version.')
|
||||
return version
|
||||
|
||||
|
||||
def download_file(url, observed, target):
|
||||
conditions = {}
|
||||
if observed['ETag'] and not observed['ETag'].startswith('W/'):
|
||||
conditions['If-Match'] = observed['ETag']
|
||||
elif observed['Last-Modified']:
|
||||
conditions['If-Unmodified-Since'] = observed['Last-Modified']
|
||||
size = 0
|
||||
prefix = b''
|
||||
with open_url(url, headers=conditions) as response:
|
||||
if metadata(response.headers) != observed:
|
||||
raise ValueError('Datei hat sich zwischen HEAD und GET geändert; erneut versuchen.')
|
||||
if response.headers.get('Content-Type', '').split(';')[0].lower() in (
|
||||
'text/html', 'application/json'):
|
||||
raise ValueError('Download liefert HTML/JSON statt einer Programmdatei.')
|
||||
expected = response.headers.get('Content-Length')
|
||||
total = int(expected) if expected is not None else None
|
||||
last_report = time.monotonic()
|
||||
if total is not None:
|
||||
print(f' Download gestartet: {total / 1048576:.1f} MiB.', flush=True)
|
||||
else:
|
||||
print(' Download gestartet; Gesamtgrösse unbekannt.', flush=True)
|
||||
with target.open('wb') as output:
|
||||
while chunk := response.read(1024 * 1024):
|
||||
if not size:
|
||||
prefix = chunk[:4]
|
||||
output.write(chunk)
|
||||
size += len(chunk)
|
||||
now = time.monotonic()
|
||||
if now - last_report >= 5:
|
||||
progress = f'{size / 1048576:.1f} MiB'
|
||||
if total:
|
||||
progress += f' / {total / 1048576:.1f} MiB ({size / total:.0%})'
|
||||
print(f' Heruntergeladen: {progress}', flush=True)
|
||||
last_report = now
|
||||
if not size or (expected is not None and size != int(expected)):
|
||||
raise ValueError('Leerer oder unvollständiger Download.')
|
||||
suffix = Path(urlsplit(url).path).suffix.lower()
|
||||
if suffix == '.exe' and not prefix.startswith(b'MZ'):
|
||||
raise ValueError('Download hat keine Windows-EXE-Signatur.')
|
||||
if suffix == '.zip' and not prefix.startswith(b'PK'):
|
||||
raise ValueError('Download hat keine ZIP-Signatur.')
|
||||
print(f' Download abgeschlossen: {size / 1048576:.1f} MiB.', flush=True)
|
||||
return size
|
||||
|
||||
|
||||
def hash_file(path):
|
||||
total = path.stat().st_size
|
||||
print(f' SHA-256-Berechnung gestartet: {total / 1048576:.1f} MiB.', flush=True)
|
||||
digest = hashlib.sha256()
|
||||
size = 0
|
||||
last_report = time.monotonic()
|
||||
with path.open('rb') as source:
|
||||
while chunk := source.read(1024 * 1024):
|
||||
digest.update(chunk)
|
||||
size += len(chunk)
|
||||
now = time.monotonic()
|
||||
if now - last_report >= 5:
|
||||
print(f' Gehasht: {size / 1048576:.1f} / {total / 1048576:.1f} MiB '
|
||||
f'({size / total:.0%})', flush=True)
|
||||
last_report = now
|
||||
print(' SHA-256-Berechnung abgeschlossen.', flush=True)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def download_hash(url, observed):
|
||||
with tempfile.TemporaryDirectory(prefix='scoop-update-') as directory:
|
||||
target = Path(directory) / 'download.bin'
|
||||
size = download_file(url, observed, target)
|
||||
return hash_file(target), size
|
||||
|
||||
|
||||
def save_manifest(path, manifest):
|
||||
original = path.read_bytes()
|
||||
newline = '\r\n' if b'\r\n' in original else '\n'
|
||||
content = json.dumps(manifest, ensure_ascii=False, indent=4) + '\n'
|
||||
temporary = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(dir=path.parent, suffix='.tmp', delete=False) as output:
|
||||
temporary = Path(output.name)
|
||||
output.write(content.replace('\n', newline).encode('utf-8'))
|
||||
temporary.replace(path)
|
||||
finally:
|
||||
if temporary is not None:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def update_manifest(path, write=False):
|
||||
manifest = json.loads(path.read_text(encoding='utf-8-sig'))
|
||||
checkver, autoupdate = manifest.get('checkver'), manifest.get('autoupdate')
|
||||
if not checkver or not autoupdate:
|
||||
print(f'{path.stem}: kein automatisches Update konfiguriert; übersprungen.')
|
||||
return False
|
||||
if not isinstance(checkver, dict) or not isinstance(autoupdate.get('url'), str):
|
||||
raise ValueError('Diese checkver/autoupdate-Konfiguration wird noch nicht unterstützt.')
|
||||
|
||||
template = autoupdate['url']
|
||||
observed = None
|
||||
if checkver.get('regex') == TIMESTAMP_REGEX:
|
||||
source = urldefrag(manifest['url'])[0]
|
||||
print(f'{path.stem}: HEAD-Anfrage an {source} ...', flush=True)
|
||||
observed = read_head(source)
|
||||
if not observed['Last-Modified']:
|
||||
raise ValueError('Last-Modified-Header fehlt.')
|
||||
date = parsedate_to_datetime(observed['Last-Modified'])
|
||||
if date.tzinfo is None:
|
||||
raise ValueError('Last-Modified enthält keine Zeitzone.')
|
||||
version = date.astimezone(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
|
||||
if urldefrag(template.replace('$version', version))[0] != source:
|
||||
raise ValueError('Zeitstempel-Updates benötigen eine feste Download-URL.')
|
||||
elif 'script' not in checkver:
|
||||
print(f'{path.stem}: Release-Version abfragen: {checkver.get("url", "")} ...', flush=True)
|
||||
version = release_version(checkver)
|
||||
else:
|
||||
raise ValueError('Dieses checkver-Skript wird nicht ausgeführt oder interpretiert.')
|
||||
|
||||
if version == manifest['version']:
|
||||
print(f'{path.stem}: {version} unverändert; kein Download.')
|
||||
return False
|
||||
url = template.replace('$version', version)
|
||||
if '$' in url:
|
||||
raise ValueError('Unterstützt wird nur der Platzhalter $version.')
|
||||
source = urldefrag(url)[0]
|
||||
if observed is None:
|
||||
print(f'{path.stem}: HEAD-Anfrage an {source} ...', flush=True)
|
||||
observed = read_head(source)
|
||||
print(f'{path.stem}: neue Version {version}; GET-Download ...', flush=True)
|
||||
print(' Für den SHA-256-Hash muss die gesamte Datei heruntergeladen werden. '
|
||||
'Danach wird der Hash berechnet. Bei grossen Dateien kann dies mehrere Minuten dauern, '
|
||||
'auch im Probelauf.', flush=True)
|
||||
digest, size = download_hash(source, observed)
|
||||
print(f'{path.stem}: {manifest["version"]} -> {version}; {size} Bytes; SHA-256 {digest}')
|
||||
manifest.update(version=version, url=url, hash=digest)
|
||||
if write:
|
||||
save_manifest(path, manifest)
|
||||
print(f'{path.stem}: Manifest gespeichert.')
|
||||
else:
|
||||
print(f'{path.stem}: Probelauf; Manifest bleibt unverändert.')
|
||||
return True
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--bucket', type=Path, default=BUCKET)
|
||||
parser.add_argument('--app', action='append', help='Manifestname; mehrfach möglich, sonst alle.')
|
||||
parser.add_argument('--write', action='store_true', help='Geprüfte Änderungen speichern.')
|
||||
args = parser.parse_args(argv)
|
||||
if not args.bucket.is_dir():
|
||||
parser.error('Bucket-Verzeichnis existiert nicht.')
|
||||
if args.app and any(not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]*', app) for app in args.app):
|
||||
parser.error('--app erwartet einen Manifestnamen ohne Pfad oder .json-Endung.')
|
||||
paths = ([args.bucket / f'{app}.json' for app in args.app] if args.app else
|
||||
sorted(args.bucket.glob('*.json')))
|
||||
errors = 0
|
||||
for path in paths:
|
||||
try:
|
||||
update_manifest(path, args.write)
|
||||
except HTTPError as error:
|
||||
print(f'{path.stem}: HTTP {error.code}; Manifest unverändert.', file=sys.stderr)
|
||||
errors += 1
|
||||
except (ValueError, KeyError, TypeError, OSError, URLError) as error:
|
||||
print(f'{path.stem}: {error}; Manifest unverändert.', file=sys.stderr)
|
||||
errors += 1
|
||||
return int(errors > 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,135 @@
|
||||
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()
|
||||
Reference in new issue
Block a user