Files
talkscore-asr/tests/test_updater.py
T
Vladimir BryzgalovandClaude Opus 5 9dc67bda5c talkscore-asr 0.1.0: сервис транскрибации и диаризации
Локальный FastAPI-сервис поверх GigaAM v3 и sherpa-onnx: приём аудио,
очередь задач, разделение по говорящим, постобработка терминов.

Доставка на Windows - ZIP со встроенным Python, без установки чего-либо.
Обновление кода при запуске тянется из релизов Gitea: меняется только
папка app, десятки килобайт вместо всего пакета.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 21:41:04 +05:00

129 lines
5.2 KiB
Python

"""Тесты автообновления.
Обновление трогает работающий сервис, поэтому проверяется главное:
не ставить старое поверх нового, не ставить битый архив и уметь откатиться.
"""
import hashlib
import io
import zipfile
import pytest
from app.updater import (
UpdateError,
install_update,
is_newer,
parse_version,
verify_sha256,
)
def make_zip(files: dict[str, str]) -> bytes:
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as z:
for name, text in files.items():
z.writestr(name, text)
return buf.getvalue()
class TestVersions:
def test_parses_plain_version(self):
assert parse_version("1.2.3") == (1, 2, 3)
def test_parses_with_v_prefix(self):
assert parse_version("v1.2.3") == (1, 2, 3)
def test_missing_parts_are_zero(self):
assert parse_version("2") == (2, 0, 0)
def test_garbage_is_lowest(self):
assert parse_version("не версия") == (0, 0, 0)
@pytest.mark.parametrize("remote,local", [("1.0.1", "1.0.0"), ("1.1.0", "1.0.9"),
("2.0.0", "1.99.99")])
def test_newer_versions_detected(self, remote, local):
assert is_newer(remote, local) is True
@pytest.mark.parametrize("remote,local", [("1.0.0", "1.0.0"), ("0.9.0", "1.0.0")])
def test_same_or_older_not_installed(self, remote, local):
"""Откат по времени - тоже ошибка: старое не должно затирать новое."""
assert is_newer(remote, local) is False
class TestChecksum:
def test_matching_checksum_passes(self):
data = b"payload"
verify_sha256(data, hashlib.sha256(data).hexdigest())
def test_wrong_checksum_raises(self):
with pytest.raises(UpdateError, match="контрольная сумма"):
verify_sha256(b"payload", "0" * 64)
def test_checksum_is_case_insensitive(self):
data = b"payload"
verify_sha256(data, hashlib.sha256(data).hexdigest().upper())
class TestInstall:
def test_replaces_app_directory(self, tmp_path):
base = tmp_path
(base / "app").mkdir()
(base / "app" / "main.py").write_text("старый", encoding="utf-8")
archive = make_zip({"app/main.py": "новый", "app/version.py": '__version__ = "9.9.9"'})
install_update(archive, base, verify=lambda _: True)
assert (base / "app" / "main.py").read_text(encoding="utf-8") == "новый"
def test_keeps_user_files(self, tmp_path):
"""Конфиг и словарь замен принадлежат пользователю, их трогать нельзя."""
base = tmp_path
(base / "app").mkdir()
(base / "config.toml").write_text("мои настройки", encoding="utf-8")
(base / "replacements.txt").write_text("мои замены", encoding="utf-8")
archive = make_zip({"app/main.py": "новый",
"config.toml": "чужие настройки",
"replacements.txt": "чужие замены"})
install_update(archive, base, verify=lambda _: True)
assert (base / "config.toml").read_text(encoding="utf-8") == "мои настройки"
assert (base / "replacements.txt").read_text(encoding="utf-8") == "мои замены"
def test_rolls_back_when_new_code_is_broken(self, tmp_path):
"""Если новая версия не запускается, возвращаем предыдущую."""
base = tmp_path
(base / "app").mkdir()
(base / "app" / "main.py").write_text("рабочий код", encoding="utf-8")
archive = make_zip({"app/main.py": "сломанный код"})
with pytest.raises(UpdateError, match="проверк"):
install_update(archive, base, verify=lambda _: False)
assert (base / "app" / "main.py").read_text(encoding="utf-8") == "рабочий код"
def test_rejects_archive_without_app(self, tmp_path):
base = tmp_path
(base / "app").mkdir()
archive = make_zip({"readme.txt": "мусор"})
with pytest.raises(UpdateError, match="нет папки app"):
install_update(archive, base, verify=lambda _: True)
def test_rejects_path_traversal(self, tmp_path):
"""Архив не должен уметь писать за пределы папки сервиса."""
base = tmp_path
(base / "app").mkdir()
archive = make_zip({"app/../../evil.py": "вредонос"})
with pytest.raises(UpdateError, match="путь"):
install_update(archive, base, verify=lambda _: True)
def test_rejects_absolute_paths(self, tmp_path):
base = tmp_path
(base / "app").mkdir()
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as z:
z.writestr("/etc/passwd", "вредонос")
z.writestr("app/main.py", "код")
with pytest.raises(UpdateError, match="путь"):
install_update(buf.getvalue(), base, verify=lambda _: True)