zapretgui/tests/test_hosts_ipv6_gating.py
loop-uh 750f7304fa
All checks were successful
Zapret source guards / validate (push) Successful in 7s
Windows-сборка / Dev Dev 21.1.6.27 опубликован
Hosts меняется только по кнопке «Применить»: щелчки копят черновик с предпросмотром строк, видно всё, что лежит в hosts; Telegram Proxy больше не пишет в hosts сам при открытии
Страница Hosts:
- Открывается сразу из готового снимка в памяти (его собирает прогрев),
  свежесть файла проверяет фоновая задача. Снимок только читает: каталог,
  текст hosts и признаки доступа; файл и папка etc не создаются.
- Переключатели меняют только черновик. Панель внизу показывает
  «+N / −M строк», сами строки, «Отменить» и «Применить» — запись одной
  операцией, после неё снимок перечитывается.
- Список сервисов — один рисуемый виджет с поиском, фильтрами (подписаны
  для экранного диктора) и «DNS для всех».
- Раздел «Что сейчас в hosts»: блоки ZapretGUI, Telegram, MAX, госСМИ,
  Adobe и строки пользователя; переключатель Adobe тоже идёт в черновик.
- Лишние строки блока ZapretGUI (не относятся ни к одному включённому
  сервису) черновик видит сам и уберёт при записи.
- Профиль сервиса определяется по самому полному совпадению строк, а при
  одинаковых адресах — по сохранённому выбору: после записи comss_dns у
  Truth Social страница больше не показывает xbox_dns.
- Чтение файла, начатое до «Применить», не затирает свежий результат.
- Защиту «только чтение» программа сама не снимает, чужие строки с теми
  же доменами не удаляет — блок ZapretGUI просто стоит выше.
- Восемь отдельных фоновых задач заменены одной HostsCallWorker; старые
  workflow, воркеры, матрица сервисов и неиспользуемые методы HostsManager
  удалены.

Telegram Proxy: при открытии страница только показывает, прописаны ли
записи Telegram в hosts; менять файл — кнопками «Прописать» и «Убрать».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:07:30 +03:00

161 lines
6.3 KiB
Python

import sys
import unittest
from pathlib import Path
from unittest.mock import patch
PUBLIC_ROOT = Path(__file__).resolve().parents[1]
SRC_ROOT = PUBLIC_ROOT / "src"
if str(SRC_ROOT) not in sys.path:
sys.path.insert(0, str(SRC_ROOT))
class Ipv6DetectionPredicateTests(unittest.TestCase):
def setUp(self) -> None:
from hosts import ipv6_detection
self.ipv6_detection = ipv6_detection
ipv6_detection.reset_ipv6_detection_cache()
self.addCleanup(ipv6_detection.reset_ipv6_detection_cache)
def test_gateway_predicate_accepts_link_local_router(self) -> None:
# Шлюз из Router Advertisement — почти всегда link-local: это норма.
self.assertTrue(self.ipv6_detection._is_usable_gateway_ipv6("fe80::1"))
self.assertTrue(
self.ipv6_detection._is_usable_gateway_ipv6("fe80::60f0:7326:977e:9215")
)
self.assertTrue(self.ipv6_detection._is_usable_gateway_ipv6("2a00:1450::1"))
def test_gateway_predicate_rejects_unusable_addresses(self) -> None:
for value in ("", "::", "::1", "ff02::1", "not-an-ip", "192.168.1.1"):
with self.subTest(value=value):
self.assertFalse(self.ipv6_detection._is_usable_gateway_ipv6(value))
def test_local_predicate_requires_global_address(self) -> None:
self.assertTrue(self.ipv6_detection._is_usable_local_ipv6("2a00:1450::1"))
self.assertTrue(
self.ipv6_detection._is_usable_local_ipv6("2606:50c0:8000::154")
)
def test_local_predicate_rejects_non_global_addresses(self) -> None:
cases = {
"ULA": "fd00::1",
"link-local": "fe80::1",
"loopback": "::1",
"unspecified": "::",
"multicast": "ff02::1",
"teredo": "2001::1",
"6to4": "2002:0101:0101::1",
"documentation": "2001:db8::1",
"ipv4-mapped": "::ffff:8.8.8.8",
"ipv4": "8.8.8.8",
"invalid": "junk",
}
for label, value in cases.items():
with self.subTest(label=label):
self.assertFalse(self.ipv6_detection._is_usable_local_ipv6(value))
def test_availability_result_is_cached_until_reset(self) -> None:
with (
patch.object(self.ipv6_detection.os, "name", "posix"),
patch.object(
self.ipv6_detection, "_is_ipv6_available_socket_probe", return_value=True
) as probe,
):
self.assertTrue(self.ipv6_detection.is_ipv6_available())
self.assertTrue(self.ipv6_detection.is_ipv6_available())
self.assertEqual(probe.call_count, 1)
self.ipv6_detection.reset_ipv6_detection_cache()
self.assertTrue(self.ipv6_detection.is_ipv6_available())
self.assertEqual(probe.call_count, 2)
def _make_profile_index(service_name: str, candidate_ips: list[str]) -> dict[str, object]:
return {
"dns_profiles": ["hosts"],
"dns_profile_names": {"hosts": "Вкл. (активировать hosts)"},
"services": [service_name],
"available_by_service": {service_name: ["hosts"]},
"has_proxy_by_service": {service_name: False},
"category_by_service": {service_name: "direct"},
"icon_by_service": {service_name: ("fa5b.github", "#181717")},
"service_id_by_name": {service_name: "hosts.test_service"},
"direct_profile": "hosts",
"domain_names_by_service": {service_name: ["raw.githubusercontent.com"]},
"profile_domain_ip_candidates_by_service": {
service_name: {"hosts": {"raw.githubusercontent.com": list(candidate_ips)}}
},
}
class ServicesCatalogPlanIpv6GateTests(unittest.TestCase):
SERVICE = "GitHub IPv6"
def _build_plan(self, *, candidate_ips, ipv6_available, current_selection):
from hosts import page_plans, proxy_domains
with patch.object(
proxy_domains,
"get_services_profile_index",
return_value=_make_profile_index(self.SERVICE, candidate_ips),
):
return page_plans.build_services_catalog_plan(
current_selection=current_selection,
active_domains_map={},
direct_title="Напрямую из hosts",
ai_title="ИИ",
other_title="Остальные",
ipv6_available=ipv6_available,
)
def _single_row(self, plan):
self.assertEqual(len(plan.groups), 1)
self.assertEqual(len(plan.groups[0].rows), 1)
return plan.groups[0].rows[0]
def test_ipv6_only_service_is_disabled_without_ipv6(self) -> None:
plan = self._build_plan(
candidate_ips=["2606:50c0:8000::154", "2606:50c0:8001::154"],
ipv6_available=False,
current_selection={self.SERVICE: "hosts"},
)
row = self._single_row(plan)
self.assertFalse(row.toggle_enabled)
self.assertFalse(row.toggle_checked)
self.assertIsNotNone(row.unavailable_reason)
def test_disabled_service_keeps_saved_selection(self) -> None:
plan = self._build_plan(
candidate_ips=["2606:50c0:8000::154"],
ipv6_available=False,
current_selection={self.SERVICE: "hosts"},
)
self.assertEqual(plan.new_selection, {self.SERVICE: "hosts"})
self.assertFalse(plan.selection_changed)
def test_ipv6_only_service_is_enabled_with_ipv6(self) -> None:
plan = self._build_plan(
candidate_ips=["2606:50c0:8000::154"],
ipv6_available=True,
current_selection={self.SERVICE: "hosts"},
)
row = self._single_row(plan)
self.assertTrue(row.toggle_enabled)
self.assertTrue(row.toggle_checked)
self.assertIsNone(row.unavailable_reason)
self.assertFalse(plan.selection_changed)
def test_mixed_service_is_not_gated_without_ipv6(self) -> None:
plan = self._build_plan(
candidate_ips=["185.199.108.133", "2606:50c0:8000::154"],
ipv6_available=False,
current_selection={self.SERVICE: "hosts"},
)
row = self._single_row(plan)
self.assertTrue(row.toggle_enabled)
self.assertTrue(row.toggle_checked)
self.assertIsNone(row.unavailable_reason)
if __name__ == "__main__":
unittest.main()