zapretgui/tests/test_preset_data_safety_guards.py
loop-uh 8d4abf1034
All checks were successful
Zapret source guards / validate (push) Successful in 9s
Windows-сборка / Dev Dev 21.1.7.54 опубликован
Список готовых стратегий написан заново: советуемые из готовых пресетов, перебор «следующая», поиск и отборы
Что видит человек на странице профиля:
- первая группа «Советуем для этого сервиса»: стратегии, которые в готовых
  пресетах стоят на этом же сервисе, самые частые выше; метка «в N пресетах»;
- панель над списком: какая стратегия выбрана, кнопки «Работает» и
  «Не работает — следующая» (включает следующую из советуемых);
- поиск всегда на виду и ищет ещё по способу словами, описанию и автору;
  быстрые отборы: советуемые, работают у меня, не пробовал, избранное;
- значок слева показывает оценку человека (не пробовал / работает /
  не работает), способ обхода написан словами под названием;
- стратегии с одним названием сложены в одну строку с кнопкой «ещё N»;
- плитки шире, в ряд не больше трёх; экскурсия описывает новый экран.

Как устроено:
- profile/strategy_list — данные без окон: сведения о стратегии (facts),
  раскладка для профиля (plan), видимые строки (rows); всё чистые функции;
- profile/strategy_usage — частота стратегий считается из самих готовых
  пресетов: сервис опознаётся по условиям профиля;
- profile/ui/strategy_list — модель, отрисовка, список, панели, виджет.
  Экран выводится из состояния одной функцией _refresh; фонового потока,
  скрытых строк и обновления «на месте» больше нет;
- каталог стратегий теперь читает label, author и description.

Удалённые profile/strategy_grouping.py, profile/strategy_list_filter.py и
profile/ui/profile_strategy_list_widget.py уже ушли из дерева в коммите
5e82c42f: этот коммит возвращает main в рабочее состояние (страница профиля
ссылалась на удалённый файл).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 23:08:35 +03:00

152 lines
6.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Данные пресетов не теряются и не подменяются тихо.
Автосинк не воскрешает отвязанную привязку, один нечитаемый файл не роняет
список, упавшая проверка не останавливает работающий DPI, действия над
профилями не уходят в другой пресет после переключения.
"""
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock, patch
class RemoteSyncBindingWriteTests(unittest.TestCase):
def _run(self, *, latest_binding):
from presets import remote_sync_workers
from presets.remote_sync import RemoteSyncOutcome
snapshot = {"url": "https://example.com/p.txt", "auto": True, "detached": False}
bindings = iter([snapshot, latest_binding])
written: list[dict] = []
outcome = RemoteSyncOutcome(status="updated", binding_updates={"checked_at": "now", "synced_hash": "h"})
with patch("presets.remote_bindings.get_remote_preset_binding", lambda *_a: next(bindings)), patch(
"presets.remote_bindings.set_remote_preset_binding",
lambda _scope, _file, binding: written.append(dict(binding)),
), patch("presets.remote_sync.sync_remote_preset", return_value=outcome):
remote_sync_workers.sync_remote_preset_by_file_name(SimpleNamespace(), "zapret2_mode", "P.txt")
return written
def test_unlinked_during_sync_binding_is_not_recreated(self) -> None:
self.assertEqual(self._run(latest_binding=None), [])
def test_auto_update_switched_off_during_sync_stays_off(self) -> None:
written = self._run(
latest_binding={"url": "https://example.com/p.txt", "auto": False, "detached": False},
)
self.assertEqual(len(written), 1)
self.assertFalse(written[0]["auto"])
self.assertEqual(written[0]["synced_hash"], "h")
def test_url_changed_during_sync_is_not_overwritten(self) -> None:
written = self._run(latest_binding={"url": "https://other.example/q.txt", "auto": True})
self.assertEqual(written, [])
class PresetListScanTests(unittest.TestCase):
def test_unreadable_file_and_txt_folder_do_not_break_list(self) -> None:
from core.paths import AppPaths
from presets import file_store as file_store_module
from presets.file_store import PresetFileStore
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
store = PresetFileStore(AppPaths(user_root=root / "user", local_root=root / "local"))
user_dir = store._engine_paths("winws2").user_presets_dir
user_dir.mkdir(parents=True, exist_ok=True)
(user_dir / "Good.txt").write_text("# Preset: Good\n--filter-tcp=443\n", encoding="utf-8")
(user_dir / "Locked.txt").write_text("# Preset: Locked\n--filter-tcp=80\n", encoding="utf-8")
(user_dir / "Folder.txt").mkdir()
real_read = file_store_module._read_header_text
def _read(path):
if path.name == "Locked.txt":
raise PermissionError(13, "locked by antivirus")
return real_read(path)
with patch.object(file_store_module, "_read_header_text", _read):
names = [manifest.file_name for manifest in store.list_manifests("winws2")]
self.assertIn("Good.txt", names)
self.assertIn("Locked.txt", names)
self.assertNotIn("Folder.txt", names)
class ValidateBeforeStopTests(unittest.TestCase):
def test_validator_crash_keeps_running_dpi(self) -> None:
from winws_runtime.runtime.preset_launch_service import PresetLaunchService
service = PresetLaunchService.__new__(PresetLaunchService)
service.launch_method = "zapret2_mode"
service.last_error_message = ""
service._progress = Mock()
with tempfile.TemporaryDirectory() as temp_dir:
preset = Path(temp_dir) / "P.txt"
preset.write_text("--wf-tcp-out=443\n--filter-tcp=443\n--lua-desync=pass\n", encoding="utf-8")
with patch(
"winws_runtime.runners.runner_factory.get_strategy_runner",
side_effect=RuntimeError("winws2.exe не найден"),
):
allowed = service._validate_preset_before_stop(
is_preset_file=True,
preset_path=str(preset),
skip_stop=False,
)
# Раньше любое исключение давало True: работающий DPI останавливался,
# а новый не стартовал — пользователь оставался вовсе без обхода.
self.assertFalse(allowed)
self.assertIn("winws2.exe не найден", service.last_error_message)
class ProfileListQueuePresetBindingTests(unittest.TestCase):
def _page(self, displayed: str):
from profile.ui.preset_setup_page import PresetSetupPageBase
page = PresetSetupPageBase.__new__(PresetSetupPageBase)
page._displayed_preset_file_name = displayed
page._cleanup_in_progress = False
page._profile_context_action_runtime = SimpleNamespace(is_running=lambda: True)
return page
def test_queued_action_remembers_preset_and_is_skipped_in_other_preset(self) -> None:
from profile.ui.preset_write_queue import PresetWriteQueue
page = self._page("A.txt")
queue = PresetWriteQueue(page)
queue._queue_profile_preset_write_operation("context", action="delete", profile_key="uid:1")
operation = page._profile_preset_write_state_obj().pop_next()
self.assertEqual(operation["preset_file_name"], "A.txt")
page._displayed_preset_file_name = "B.txt"
queue._profile_preset_write_operation_running = lambda: False
queue._start_profile_context_action_worker = Mock()
queue._run_profile_preset_write_operation(operation)
# Удаление профиля пресета A не должно выполниться в пресете B.
queue._start_profile_context_action_worker.assert_not_called()
def test_preset_switch_drops_pending_profile_actions_but_keeps_user_profiles(self) -> None:
from profile.ui.preset_write_queue import PresetWriteQueue
page = self._page("A.txt")
queue = PresetWriteQueue(page)
queue._queue_profile_preset_write_operation("context", action="delete", profile_key="uid:1")
queue._queue_profile_preset_write_operation("user_profile", action="delete", profile_id="my")
queue._drop_preset_bound_operations()
self.assertEqual(
[op["kind"] for op in page._profile_preset_write_state_obj().pending],
["user_profile"],
)
if __name__ == "__main__":
unittest.main()