zapretgui/tests/test_dns_page.py
loop-uh feb1a367ad
All checks were successful
Zapret source guards / validate (push) Successful in 7s
Настройка DNS: вместо короткой дуги — комета, которая всегда замыкает круг
DNS записывается за доли секунды, и прежняя дуга не успевала пройти и
половину оборота. Теперь по щелчку от верхней точки значка бежит комета
(яркая голова со свечением, тающий хвост) и всегда проходит полный круг
за 0,65 с; оборот значка со вспышкой — только когда круг замкнулся. Если
запись дольше, комета кружит, пока DNS не встанет. То же у большого
значка панели «Сейчас».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 13:39:50 +03:00

428 lines
20 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.

from __future__ import annotations
import os
import unittest
from types import SimpleNamespace
from unittest.mock import Mock, patch
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PyQt6.QtCore import QPoint
from PyQt6.QtWidgets import QApplication
from dataclasses import replace
from dns import page_plans
from dns.adapters import DnsAdapter
from dns.dns_providers import DNS_PROVIDERS
from dns.latency import DnsLatencyReport
from dns.state import DnsState
ETH = "{00000000-0000-0000-0000-000000000009}"
WIFI = "{00000000-0000-0000-0000-000000000012}"
SPARE = "{00000000-0000-0000-0000-000000000020}"
ETHERNET = DnsAdapter(
ETH, "Ethernet", "Intel I219-V", "ethernet", connected=True, internet=True,
static_ipv4=("1.1.1.1", "1.0.0.1"), static_ipv6=("2606:4700:4700::1111",),
)
WIFI_ADAPTER = DnsAdapter(WIFI, "Wi-Fi", "Intel AX201", "wifi", connected=True, internet=False, auto_ipv4=("192.168.1.1",))
# Отключённый адаптер виден, но не отмечен.
SPARE_ADAPTER = DnsAdapter(SPARE, "Ethernet 2", "Realtek", "ethernet", connected=False, internet=False)
STATE = DnsState(adapters=(ETHERNET, WIFI_ADAPTER, SPARE_ADAPTER), ipv6_available=False, doh_supported=True)
def _feature(state=STATE):
return SimpleNamespace(
consume_warmed_page_data=Mock(return_value=state),
create_page_load_worker=Mock(),
create_dns_apply_worker=Mock(),
create_dns_flush_cache_worker=Mock(),
create_dns_latency_worker=Mock(),
create_isp_dns_warning_worker=Mock(),
)
class DnsPageTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def setUp(self) -> None:
self.saved_servers: list[list[dict]] = []
self.custom_servers: list[dict] = []
patches = [
patch("dns.ui.page.get_custom_dns_servers", side_effect=lambda: list(self.custom_servers)),
patch("dns.ui.page.set_custom_dns_servers", side_effect=self._save_servers),
patch("dns.ui.page.InfoBar"),
patch("dns.ui.now_panel.are_live_animations_enabled", return_value=True),
patch("dns.ui.provider_grid.are_live_animations_enabled", return_value=True),
]
for item in patches:
started = item.start()
self.addCleanup(item.stop)
if item.attribute == "InfoBar":
self.info_bar = started
def _save_servers(self, servers):
self.custom_servers = list(servers)
self.saved_servers.append(list(servers))
return list(servers)
def _page(self, *, load: bool = True, state=STATE):
from dns.ui.page import NetworkPage
feature = _feature(state)
page = NetworkPage(deps=SimpleNamespace(dns_feature=feature))
self.addCleanup(page.deleteLater)
page.resize(1200, 900)
for lane_name in ("_load_lane", "_apply_lane", "_flush_lane", "_latency_lane", "_isp_lane"):
getattr(page, lane_name).request = Mock()
if load:
page.on_page_activated()
return page
@staticmethod
def _provider_tiles(page):
return [tile for tile in page.grid.tiles() if tile.kind == "provider"]
# ── построение ──────────────────────────────────────────
def test_server_tiles_are_ready_before_network_data_loads(self) -> None:
page = self._page(load=False)
total = sum(len(group) for group in DNS_PROVIDERS.values())
self.assertEqual(len(self._provider_tiles(page)), total)
self.assertEqual(page.grid.tiles()[-1].kind, "add")
self.assertEqual(page.now_panel.title_label.text(), "Загружаю настройки сети…")
self.assertTrue(page.now_panel.badge.is_busy())
page._load_lane.request.assert_not_called()
def test_warmed_data_is_used_without_loading_again(self) -> None:
page = self._page()
page._load_lane.request.assert_not_called()
page._isp_lane.request.assert_called_once_with()
self.assertEqual(page.now_panel.adapter_keys(), [ETH, WIFI, SPARE])
chips = page.now_panel._chips
self.assertEqual(chips[ETH].text(), "Ethernet · интернет")
self.assertEqual(chips[SPARE].text(), "Ethernet 2 · не подключён")
self.assertEqual([chips[key].isChecked() for key in (ETH, WIFI, SPARE)], [True, True, False])
page.on_page_activated()
page._dns.consume_warmed_page_data.assert_called_once_with()
def test_page_loads_in_background_without_warmed_data(self) -> None:
from dns.ui.page import NetworkPage
feature = _feature(None)
page = NetworkPage(deps=SimpleNamespace(dns_feature=feature))
self.addCleanup(page.deleteLater)
page._load_lane.request = Mock()
page.on_page_activated()
page.on_page_activated()
page._load_lane.request.assert_called_once_with()
# ── текущий DNS ─────────────────────────────────────────
def test_mixed_adapters_then_single_adapter_states(self) -> None:
page = self._page()
self.assertEqual(page.now_panel.title_label.text(), "На адаптерах разные DNS")
page.now_panel._chips[WIFI].setChecked(False)
self.assertEqual(page.now_panel.title_label.text(), "Cloudflare")
self.assertIn("1.1.1.1 · 1.0.0.1", page.now_panel.detail_label.text())
selected = [tile.key for tile in self._provider_tiles(page) if tile.selected]
self.assertEqual(selected, ["Cloudflare"])
page.now_panel._chips[ETH].setChecked(False)
page.now_panel._chips[WIFI].setChecked(True)
self.assertEqual(page.now_panel.title_label.text(), "Автоматически (DHCP)")
page.now_panel._chips[WIFI].setChecked(False)
self.assertEqual(page.now_panel.title_label.text(), "Адаптеры не отмечены")
def test_current_dns_plan_matches_providers(self) -> None:
providers = {"G": {"Only6": {"ipv4": [], "ipv6": ["2a00::1"]}, "Q": {"ipv4": ["9.9.9.9"], "ipv6": []}}}
base = DnsAdapter("{A}", "A", "", "ethernet", True, False)
def plan(*static):
adapters = [replace(base, static_ipv4=tuple(v4), static_ipv6=tuple(v6)) for v4, v6 in static]
return page_plans.build_current_dns_plan(adapters=adapters, providers=providers)
self.assertEqual(plan(([], ["2a00::1"]), ([], ["2a00::1"])).provider, "Only6")
self.assertEqual(plan((["9.9.9.9", "1.2.3.4"], []), (["9.9.9.9"], [])).provider, "Q")
self.assertEqual(plan((["10.0.0.1"], []), (["10.0.0.1"], [])).kind, "custom")
self.assertEqual(plan((["10.0.0.1"], []), ([], [])).kind, "mixed")
self.assertEqual(plan(([], []), ([], [])).kind, "auto")
self.assertEqual(plan().kind, "none")
# ── применение ──────────────────────────────────────────
def test_choosing_server_applies_it_to_checked_adapters_only(self) -> None:
page = self._page()
page.now_panel._chips[WIFI].setChecked(False)
page.grid.activated.emit("Google DNS")
payload = page._apply_lane.request.call_args.args[0]
self.assertEqual(payload["action"], "provider")
self.assertEqual(payload["adapters"], [ETH])
self.assertEqual(payload["name"], "Google DNS")
self.assertEqual(payload["data"]["ipv4"][0], "8.8.8.8")
self.assertFalse(payload["ipv6_available"])
pending = [tile.key for tile in self._provider_tiles(page) if tile.pending]
self.assertEqual(pending, ["Google DNS"])
self.assertTrue(page.grid.is_charging("Google DNS"))
self.assertTrue(page.now_panel.badge.is_busy())
self.assertEqual(page.now_panel.detail_label.text(), "Применяю…")
plan = page_plans.build_provider_dns_apply_result_plan(name="Google DNS", adapter_count=1, success_count=1, ipv6=[])
google = replace(ETHERNET, static_ipv4=("8.8.8.8", "8.8.4.4"), static_ipv6=())
page._on_apply_done(payload, {"plan": plan, "state": replace(STATE, adapters=(google, WIFI_ADAPTER, SPARE_ADAPTER))})
self.assertIsNone(page._pending_choice)
self.assertEqual(page.now_panel.title_label.text(), "Google DNS")
# DNS встал мгновенно: комета сначала замыкает круг, потом значок делает оборот.
self.assertTrue(page.grid.is_charging("Google DNS"))
page.grid._clock = lambda: 10**9
page.grid._tick()
self.assertFalse(page.grid.is_charging())
self.assertEqual(page.grid.settling_keys(), ["Google DNS"])
self.info_bar.warning.assert_not_called()
def test_refreshed_adapters_keep_user_checks(self) -> None:
page = self._page()
page.now_panel._chips[WIFI].setChecked(False)
page.now_panel._chips[SPARE].setChecked(True)
page._on_apply_done({}, {"plan": None, "state": STATE})
chips = page.now_panel._chips
self.assertEqual([chips[key].isChecked() for key in (ETH, WIFI, SPARE)], [True, False, True])
def test_isp_warning_looks_only_at_connected_adapters(self) -> None:
self.assertTrue(page_plans.should_show_isp_dns_warning([WIFI_ADAPTER, SPARE_ADAPTER], warning_already_shown=False))
self.assertFalse(page_plans.should_show_isp_dns_warning([WIFI_ADAPTER], warning_already_shown=True))
self.assertFalse(page_plans.should_show_isp_dns_warning([ETHERNET, WIFI_ADAPTER], warning_already_shown=False))
self.assertFalse(page_plans.should_show_isp_dns_warning([SPARE_ADAPTER], warning_already_shown=False))
def test_partial_apply_and_errors_are_reported(self) -> None:
page = self._page()
plan = page_plans.build_auto_dns_apply_result_plan(adapter_count=2, success_count=1, error="«Wi-Fi»: ошибка Windows 5")
page._on_apply_done({}, {"plan": plan, "state": None})
content = self.info_bar.warning.call_args.kwargs["content"]
self.assertIn("1 из 2", content)
self.assertIn("«Wi-Fi»: ошибка Windows 5", content)
invalid = page_plans.build_provider_dns_plan(name="X", data={"ipv4": [], "ipv6": ["2a00::1"]}, ipv6_available=False)
page._on_apply_done({}, {"plan": invalid, "state": None})
self.assertIn("нет DNS адресов", self.info_bar.warning.call_args.kwargs["content"])
page._pending_choice = "Quad9"
page._on_apply_failed({}, "Нет прав администратора")
self.assertIsNone(page._pending_choice)
self.assertEqual(self.info_bar.error.call_args.kwargs["content"], "Нет прав администратора")
def test_nothing_is_applied_without_adapters_or_before_loading(self) -> None:
page = self._page(load=False)
page.grid.activated.emit("Quad9")
self.info_bar.info.assert_called_once()
page.on_page_activated()
for chip in page.now_panel._chips.values():
chip.setChecked(False)
page.grid.activated.emit("Quad9")
page._apply_lane.request.assert_not_called()
self.assertEqual(self.info_bar.warning.call_args.kwargs["title"], "Нет отмеченных адаптеров")
def test_reset_to_auto_asks_first_and_names_dialog_buttons(self) -> None:
page = self._page()
boxes = []
class Box:
answer = False
def __init__(self, title, body, parent=None):
self.title, self.body = title, body
self.yesButton, self.cancelButton = Mock(), Mock()
boxes.append(self)
def exec(self):
return Box.answer
with patch("dns.ui.page.MessageBox", Box):
page.now_panel.reset_button.click()
page._apply_lane.request.assert_not_called()
Box.answer = True
page.now_panel.reset_button.click()
self.assertIn("DHCP", boxes[0].body)
boxes[0].yesButton.setAccessibleName.assert_called_with(boxes[0].title)
boxes[0].cancelButton.setAccessibleName.assert_called_with(f"Отменить действие: {boxes[0].title}")
payload = page._apply_lane.request.call_args.args[0]
self.assertEqual(payload, {"action": "auto", "adapters": [ETH, WIFI]})
self.assertEqual(page.now_panel.title_label.text(), "Автоматически (DHCP)")
self.assertEqual(page.now_panel.detail_label.text(), "Применяю…")
# ── фильтр ──────────────────────────────────────────────
def test_filter_shows_one_group_without_headers(self) -> None:
page = self._page()
page._set_filter("Для ИИ")
kinds = {tile.kind for tile in page.grid.tiles()}
self.assertEqual(kinds, {"provider"})
self.assertEqual(len(page.grid.tiles()), len(DNS_PROVIDERS["Для ИИ"]))
page._set_filter("Свои DNS")
self.assertEqual([tile.kind for tile in page.grid.tiles()], ["add"])
# ── замер скорости ──────────────────────────────────────
def test_latency_measurement_marks_tiles_and_fastest_server(self) -> None:
page = self._page()
page.now_panel.measure_button.click()
servers = page._latency_lane.request.call_args.args[0]
self.assertIn("1.1.1.1", servers)
self.assertTrue(all(":" not in server for server in servers))
self.assertFalse(page.now_panel.measure_button.isEnabled())
self.assertEqual({tile.latency for tile in self._provider_tiles(page)}, {"measuring"})
page._on_latency_done(DnsLatencyReport(results={"1.1.1.1": 30.0, "8.8.8.8": 9.6, "9.9.9.9": None}))
tiles = {tile.key: tile for tile in self._provider_tiles(page)}
self.assertTrue(tiles["Google DNS"].fastest)
self.assertEqual(tiles["Quad9"].latency, "timeout")
self.assertEqual(tiles["AdGuard"].latency, "")
self.assertEqual(page.latency_summary.text(), "Быстрее всех: Google DNS — 10 мс")
self.assertTrue(page.now_panel.measure_button.isEnabled())
self.assertTrue(page.now_panel.notice_label.isHidden())
def test_intercepted_dns_is_explained(self) -> None:
page = self._page()
page._measure_latency()
page._on_latency_done(DnsLatencyReport(results={"1.1.1.1": 48.0}, intercepted=True))
self.assertFalse(page.now_panel.notice_label.isHidden())
self.assertIn("перехватываются", page.now_panel.notice_label.text())
page._measure_latency()
page._on_latency_done(None)
self.assertEqual(page.latency_summary.text(), "Замер не удался")
# ── предупреждение про DNS провайдера ───────────────────
def test_isp_warning_offers_quad9_in_top_right_infobar(self) -> None:
page = self._page()
bar = Mock()
self.info_bar.warning.return_value = bar
plan = page_plans.build_isp_dns_warning_plan([WIFI_ADAPTER], warning_already_shown=False)
page._show_isp_warning(plan)
kwargs = self.info_bar.warning.call_args.kwargs
self.assertEqual(kwargs["duration"], 10000)
self.assertEqual(kwargs["position"].name, "TOP_RIGHT")
self.assertLessEqual(max(len(line) for line in kwargs["content"].splitlines()), 86)
button = bar.addWidget.call_args.args[0]
button.click()
bar.close.assert_called_once_with()
self.assertEqual(page._apply_lane.request.call_args.args[0]["name"], "Quad9")
def test_isp_warning_is_skipped_when_not_needed(self) -> None:
page = self._page()
page._show_isp_warning(SimpleNamespace(should_show=False))
self.info_bar.warning.assert_not_called()
# ── свои DNS ────────────────────────────────────────────
def test_custom_server_add_edit_duplicate_copy_delete(self) -> None:
page = self._page()
page._ipv6_available = True
dialogs = []
class Dialog:
result = {"id": "custom-1", "name": "Дом", "ipv4": ["192.168.1.1"], "ipv6": ["fd00::1"]}
def __init__(self, parent=None, *, server=None, ipv6_available=False):
dialogs.append((server, ipv6_available))
def exec(self):
return True
def server(self):
return dict(Dialog.result)
with patch("dns.ui.page.CustomDnsDialog", Dialog):
page.grid.add_clicked.emit()
self.assertEqual(dialogs[-1], (None, True))
self.assertIn("Дом", [tile.key for tile in self._provider_tiles(page)])
Dialog.result = {"id": "custom-1", "name": "Дача", "ipv4": ["10.0.0.1"], "ipv6": []}
self._run_menu(page, "Дом", "Редактировать")
self.assertEqual(dialogs[-1][0]["name"], "Дом")
self.assertEqual([server["name"] for server in self.custom_servers], ["Дача"])
self._run_menu(page, "Дача", "Создать копию")
self.assertEqual(len(self.custom_servers), 2)
self.assertNotEqual(self.custom_servers[1]["id"], "custom-1")
self._run_menu(page, "Дача", "Копировать DNS в буфер обмена")
self.assertEqual(QApplication.clipboard().text(), "10.0.0.1")
self._run_menu(page, "Дача", "Удалить")
self.assertEqual(len(self.custom_servers), 1)
self.assertNotIn("Дача", [tile.key for tile in self._provider_tiles(page)])
def _run_menu(self, page, name: str, text: str) -> None:
def choose(menu, _pos, **_kwargs):
return next(action for action in menu.actions() if action.text() == text)
with patch("dns.ui.page.exec_popup_menu", side_effect=choose):
page.grid.context_menu_wanted.emit(name, QPoint(0, 0))
# ── прочее ──────────────────────────────────────────────
def test_flush_cache_reports_result(self) -> None:
page = self._page()
page.now_panel.flush_button.click()
page._flush_lane.request.assert_called_once_with()
self.assertFalse(page.now_panel.flush_button.isEnabled())
page._on_flush_done(page_plans.build_flush_dns_cache_result_plan(success=True, message=""))
self.assertTrue(page.now_panel.flush_button.isEnabled())
self.assertEqual(self.info_bar.success.call_args.kwargs["title"], "Кэш DNS очищен")
def test_english_interface(self) -> None:
page = self._page()
page.set_ui_language("en")
self.assertEqual(page.now_panel.measure_button.text(), "Measure speed")
self.assertEqual(page.now_panel.title_label.text(), "Adapters use different DNS")
self.assertEqual(page.grid.tiles()[0].title, "Popular")
def test_cleanup_closes_every_lane(self) -> None:
page = self._page()
lanes = [page._load_lane, page._apply_lane, page._flush_lane, page._latency_lane, page._isp_lane]
for lane in lanes:
lane.close = Mock()
page.cleanup()
for lane in lanes:
lane.close.assert_called_once_with()
page._on_page_data(STATE)
if __name__ == "__main__":
unittest.main()