zapretgui/tests/test_remote_preset_sync.py
loop-uh c93368bc47
All checks were successful
Zapret source guards / validate (push) Successful in 5s
Добавить удалённые пресеты: импорт по ссылке и автообновление
Новый fluent-диалог импорта (drag-and-drop файла + вставка ссылки),
скачивание в фоне с лимитами и понятными ошибками. Пресет по https-ссылке
привязывается к источнику (settings store, файл остаётся чистым) и
автообновляется: при старте и раз в 6 часов, условные запросы ETag,
локальные правки ставят автосинк на паузу. Активный пресет обновляется
штатным путём записи с перезапуском стратегии и уведомлением. Один URL =
один пресет: повторный импорт ссылки обновляет существующий без дублей.
В списке пресеты с привязкой помечены облаком, в меню — «Обновить из
источника» и «Отвязать от источника».

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 22:01:52 +03:00

282 lines
11 KiB
Python

"""Ядро автосинка удалённых пресетов: решения, хэши, лимиты, конфликты."""
from __future__ import annotations
import unittest
from unittest.mock import Mock, patch
import requests
from presets.remote_sync import (
AUTO_CHECK_INTERVAL_SECONDS,
MAX_REMOTE_PRESET_BYTES,
STATUS_DETACHED,
STATUS_ERROR,
STATUS_NOT_MODIFIED,
STATUS_SKIPPED,
STATUS_UNCHANGED,
STATUS_UPDATED,
RemoteFetchResult,
RemoteSyncFetchError,
comparison_hash,
fetch_remote_preset_text,
should_auto_check,
sync_remote_preset,
)
NOW_ISO = "2026-08-14T12:00:00Z"
VALID_WINWS2_TEXT = (
"# Preset: Example\n"
"--wf-tcp-out=80,443\n"
"--lua-desync=hostfakesplit:host=x.com:tcp_ts=-1000\n"
)
UPDATED_WINWS2_TEXT = (
"# Preset: Example\n"
"--wf-tcp-out=80,443,8080\n"
"--lua-desync=hostfakesplit:host=y.com:tcp_ts=-500\n"
)
def _binding(**overrides):
base = {
"url": "https://example.com/p.txt",
"etag": "",
"last_modified": "",
"synced_hash": comparison_hash(VALID_WINWS2_TEXT),
"checked_at": "",
"updated_at": "",
"error": "",
"auto": True,
"detached": False,
}
base.update(overrides)
return base
def _sync(binding, *, current_text=VALID_WINWS2_TEXT, fetch=None, save=None, force=False):
save = save or Mock(return_value=None)
outcome = sync_remote_preset(
binding,
engine="winws2",
read_current_text=lambda: current_text,
fetch=fetch or Mock(return_value=RemoteFetchResult(status_code=304)),
save_text=save,
now_iso=NOW_ISO,
force=force,
)
return outcome, save
class ComparisonHashTests(unittest.TestCase):
def test_ignores_comments_and_debug_line(self):
base = comparison_hash(VALID_WINWS2_TEXT)
renamed = VALID_WINWS2_TEXT.replace("# Preset: Example", "# Preset: Renamed")
with_debug = VALID_WINWS2_TEXT + "--debug=@C:\\logs\\preset.log\n"
crlf = VALID_WINWS2_TEXT.replace("\n", "\r\n")
self.assertEqual(comparison_hash(renamed), base)
self.assertEqual(comparison_hash(with_debug), base)
self.assertEqual(comparison_hash(crlf), base)
def test_detects_meaningful_change(self):
self.assertNotEqual(
comparison_hash(VALID_WINWS2_TEXT), comparison_hash(UPDATED_WINWS2_TEXT)
)
class ShouldAutoCheckTests(unittest.TestCase):
def _parse(self, value):
return {"old": 0.0, "fresh": 1000.0}.get(value)
def test_never_checked_is_due(self):
self.assertTrue(should_auto_check(_binding(), now_ts=1.0, parse_ts=self._parse))
def test_interval_gate(self):
binding = _binding(checked_at="old")
self.assertTrue(
should_auto_check(binding, now_ts=AUTO_CHECK_INTERVAL_SECONDS + 1, parse_ts=self._parse)
)
binding = _binding(checked_at="fresh")
self.assertFalse(should_auto_check(binding, now_ts=1500.0, parse_ts=self._parse))
def test_disabled_or_detached(self):
self.assertFalse(should_auto_check(_binding(auto=False), now_ts=1e12, parse_ts=self._parse))
self.assertFalse(should_auto_check(_binding(detached=True), now_ts=1e12, parse_ts=self._parse))
def test_unparsable_checked_at_is_due(self):
self.assertTrue(
should_auto_check(_binding(checked_at="garbage"), now_ts=1.0, parse_ts=self._parse)
)
class SyncRemotePresetTests(unittest.TestCase):
def test_not_modified_updates_checked_at_only(self):
binding = _binding(etag='W/"1"')
fetch = Mock(return_value=RemoteFetchResult(status_code=304))
outcome, save = _sync(binding, fetch=fetch)
self.assertEqual(outcome.status, STATUS_NOT_MODIFIED)
self.assertEqual(outcome.binding_updates["checked_at"], NOW_ISO)
save.assert_not_called()
fetch.assert_called_once_with("https://example.com/p.txt", 'W/"1"', "")
def test_unchanged_content_does_not_save(self):
fetch = Mock(
return_value=RemoteFetchResult(status_code=200, text=VALID_WINWS2_TEXT, etag='W/"2"')
)
outcome, save = _sync(_binding(), fetch=fetch)
self.assertEqual(outcome.status, STATUS_UNCHANGED)
self.assertEqual(outcome.binding_updates["etag"], 'W/"2"')
save.assert_not_called()
def test_updated_content_saves_via_pipeline(self):
fetch = Mock(return_value=RemoteFetchResult(status_code=200, text=UPDATED_WINWS2_TEXT))
outcome, save = _sync(_binding(), fetch=fetch)
self.assertEqual(outcome.status, STATUS_UPDATED)
save.assert_called_once_with(UPDATED_WINWS2_TEXT)
self.assertEqual(outcome.binding_updates["updated_at"], NOW_ISO)
self.assertEqual(
outcome.binding_updates["synced_hash"], comparison_hash(UPDATED_WINWS2_TEXT)
)
self.assertFalse(outcome.binding_updates["detached"])
def test_synced_hash_uses_saved_normalized_text(self):
normalized = UPDATED_WINWS2_TEXT + "# normalized-marker\n"
fetch = Mock(return_value=RemoteFetchResult(status_code=200, text=UPDATED_WINWS2_TEXT))
save = Mock(return_value=normalized)
outcome, _ = _sync(_binding(), fetch=fetch, save=save)
self.assertEqual(outcome.binding_updates["synced_hash"], comparison_hash(normalized))
def test_invalid_remote_text_is_error_without_save(self):
fetch = Mock(return_value=RemoteFetchResult(status_code=200, text="случайный текст\n"))
outcome, save = _sync(_binding(), fetch=fetch)
self.assertEqual(outcome.status, STATUS_ERROR)
self.assertIn("не похож на пресет", outcome.binding_updates["error"])
save.assert_not_called()
def test_winws2_requires_lua_desync(self):
no_lua = "# Preset: X\n--wf-tcp-out=443\n"
fetch = Mock(return_value=RemoteFetchResult(status_code=200, text=no_lua))
outcome, save = _sync(_binding(), fetch=fetch)
self.assertEqual(outcome.status, STATUS_ERROR)
save.assert_not_called()
def test_local_edit_detaches_without_touching_file(self):
edited = VALID_WINWS2_TEXT + "--filter-tcp=8080\n"
fetch = Mock()
outcome, save = _sync(_binding(), current_text=edited, fetch=fetch)
self.assertEqual(outcome.status, STATUS_DETACHED)
self.assertTrue(outcome.binding_updates["detached"])
fetch.assert_not_called()
save.assert_not_called()
def test_detached_binding_is_skipped(self):
edited = VALID_WINWS2_TEXT + "--filter-tcp=8080\n"
outcome, save = _sync(_binding(detached=True), current_text=edited)
self.assertEqual(outcome.status, STATUS_SKIPPED)
self.assertEqual(outcome.binding_updates, {})
save.assert_not_called()
def test_force_overwrites_local_edits_and_reattaches(self):
edited = VALID_WINWS2_TEXT + "--filter-tcp=8080\n"
fetch = Mock(return_value=RemoteFetchResult(status_code=200, text=UPDATED_WINWS2_TEXT))
outcome, save = _sync(_binding(detached=True), current_text=edited, fetch=fetch, force=True)
self.assertEqual(outcome.status, STATUS_UPDATED)
save.assert_called_once_with(UPDATED_WINWS2_TEXT)
self.assertFalse(outcome.binding_updates["detached"])
fetch.assert_called_once_with("https://example.com/p.txt", "", "")
def test_fetch_error_recorded_in_binding(self):
fetch = Mock(side_effect=RemoteSyncFetchError("HTTP 404"))
outcome, save = _sync(_binding(), fetch=fetch)
self.assertEqual(outcome.status, STATUS_ERROR)
self.assertEqual(outcome.binding_updates["error"], "HTTP 404")
save.assert_not_called()
def test_missing_file_is_error(self):
outcome = sync_remote_preset(
_binding(),
engine="winws2",
read_current_text=lambda: None,
fetch=Mock(),
save_text=Mock(),
now_iso=NOW_ISO,
)
self.assertEqual(outcome.status, STATUS_ERROR)
class _FakeResponse:
def __init__(self, *, status_code=200, headers=None, chunks=None):
self.status_code = status_code
self.headers = dict(headers or {})
self._chunks = list(chunks or [])
self.closed = False
def iter_content(self, chunk_size):
yield from self._chunks
def close(self):
self.closed = True
class FetchRemotePresetTextTests(unittest.TestCase):
def _patch(self, response=None, error=None):
def _fake(url, **kwargs):
if error is not None:
raise error
self.last_kwargs = kwargs
return response
return patch("presets.remote_sync.request_get_bypass_proxy", side_effect=_fake)
def test_https_only(self):
with self.assertRaises(RemoteSyncFetchError):
fetch_remote_preset_text("http://example.com/p.txt")
def test_conditional_headers_sent(self):
response = _FakeResponse(status_code=304)
with self._patch(response):
result = fetch_remote_preset_text(
"https://example.com/p.txt", etag='W/"9"', last_modified="Thu, 01 Jan 2026"
)
self.assertEqual(result.status_code, 304)
headers = self.last_kwargs["headers"]
self.assertEqual(headers["If-None-Match"], 'W/"9"')
self.assertEqual(headers["If-Modified-Since"], "Thu, 01 Jan 2026")
def test_success_returns_text_and_validators(self):
response = _FakeResponse(
headers={"ETag": 'W/"5"', "Last-Modified": "Fri, 02 Jan 2026"},
chunks=[VALID_WINWS2_TEXT.encode("utf-8")],
)
with self._patch(response):
result = fetch_remote_preset_text("https://example.com/p.txt")
self.assertEqual(result.text, VALID_WINWS2_TEXT)
self.assertEqual(result.etag, 'W/"5"')
self.assertEqual(result.last_modified, "Fri, 02 Jan 2026")
self.assertTrue(response.closed)
def test_size_limit(self):
big = b"x" * (MAX_REMOTE_PRESET_BYTES + 1)
with self._patch(_FakeResponse(chunks=[big])):
with self.assertRaises(RemoteSyncFetchError):
fetch_remote_preset_text("https://example.com/p.txt")
def test_zip_content_rejected(self):
with self._patch(_FakeResponse(chunks=[b"PK\x03\x04zip"])):
with self.assertRaises(RemoteSyncFetchError):
fetch_remote_preset_text("https://example.com/p.txt")
def test_http_status_error(self):
with self._patch(_FakeResponse(status_code=500)):
with self.assertRaises(RemoteSyncFetchError):
fetch_remote_preset_text("https://example.com/p.txt")
def test_network_error(self):
with self._patch(error=requests.exceptions.ConnectionError("refused")):
with self.assertRaises(RemoteSyncFetchError):
fetch_remote_preset_text("https://example.com/p.txt")
if __name__ == "__main__":
unittest.main()