All checks were successful
Zapret source guards / validate (push) Successful in 11s
Windows спрашивает все DNS-серверы, которые у неё есть, и берёт первый ответ. Поэтому выбранный сервер (в том числе шифрованный) «не спасает», если рядом остался другой: - у адаптера с выбранным сервером по IPv4 остаётся автоматический DNS по IPv6 от роутера или провайдера — так выходит, когда у выбранного сервера адресов IPv6 нет; - подключённый, но не отмеченный адаптер держит собственный DNS. Под панелью «Сейчас DNS» теперь об этом сказано прямо, с названием адаптера и адресами. Заглушки Windows fec0:0:0:ffff::1…3 за DNS не считаются; пока сервер не выбран («Автоматически»), предупреждения нет. Предупреждение о перехвате по-прежнему главнее. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
980 lines
52 KiB
Python
980 lines
52 KiB
Python
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, Qt
|
||
from PyQt6.QtTest import QTest
|
||
from PyQt6.QtWidgets import QApplication, QMainWindow
|
||
|
||
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 CustomServerResult, DnsState, LineCheck
|
||
from config.urls import ZAPRET_DNS_SITE_URL
|
||
from dns.ui.page import AUTO_CHOICE, PROMO_PROVIDER
|
||
from dns.ui.provider_grid import PROMO_TILE_KEY
|
||
|
||
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)
|
||
|
||
HOME = {"id": "custom-1", "name": "Дом", "ipv4": ["192.168.1.1"], "ipv6": ["fd00::1"], "doh": ""}
|
||
SECURE = {
|
||
"id": "custom-2",
|
||
"name": "dns.example.com",
|
||
"ipv4": ["203.0.113.5"],
|
||
"ipv6": [],
|
||
"doh": "https://dns.example.com/dns-query",
|
||
}
|
||
|
||
|
||
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_dns_answering_worker=Mock(),
|
||
create_isp_dns_warning_worker=Mock(),
|
||
create_custom_server_worker=Mock(),
|
||
)
|
||
|
||
|
||
class DnsPageTests(unittest.TestCase):
|
||
@classmethod
|
||
def setUpClass(cls) -> None:
|
||
cls._app = QApplication.instance() or QApplication([])
|
||
|
||
def setUp(self) -> None:
|
||
patches = [
|
||
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 _page(self, *, load: bool = True, state=STATE):
|
||
from dns.ui.page import NetworkPage
|
||
|
||
feature = _feature(state)
|
||
self.open_custom_server = Mock()
|
||
self.create_open_url_worker = Mock()
|
||
page = NetworkPage(
|
||
deps=SimpleNamespace(
|
||
dns_feature=feature,
|
||
open_custom_server=self.open_custom_server,
|
||
create_open_url_worker=self.create_open_url_worker,
|
||
)
|
||
)
|
||
self.addCleanup(page.deleteLater)
|
||
page.resize(1200, 900)
|
||
for lane in page._lanes():
|
||
lane.request = Mock()
|
||
if load:
|
||
page.on_page_activated()
|
||
return page
|
||
|
||
# ── экскурсия ────────────────────────────────────────────
|
||
|
||
def test_tour_opens_ai_group_and_returns_the_previous_filter(self) -> None:
|
||
page = self._page()
|
||
page.filter_bar.setCurrentItem("Безопасные")
|
||
page._set_filter("Безопасные")
|
||
self.assertIsNone(page.onboarding_target("ai"))
|
||
|
||
page.onboarding_set_state("ai")
|
||
|
||
self.assertEqual(page.onboarding_target("ai"), [page.filter_row, page.grid])
|
||
self.assertEqual({tile.key for tile in self._provider_tiles(page)}, set(DNS_PROVIDERS["Для ИИ"]))
|
||
self.assertEqual(page.filter_bar.currentRouteKey(), "Для ИИ")
|
||
|
||
page.onboarding_set_state(None)
|
||
|
||
self.assertEqual(page._filter, "Безопасные")
|
||
self.assertEqual(page.filter_bar.currentRouteKey(), "Безопасные")
|
||
self.assertEqual({tile.key for tile in self._provider_tiles(page)}, set(DNS_PROVIDERS["Безопасные"]))
|
||
|
||
def test_tour_points_at_filters_with_the_automatic_tile_and_opens_empty_form(self) -> None:
|
||
page = self._page()
|
||
|
||
filter_row, (grid, rect) = page.onboarding_target("providers")
|
||
|
||
self.assertIs(filter_row, page.filter_row)
|
||
self.assertIs(grid, page.grid)
|
||
# Подсвечена первая строка сетки: «Автоматически» вместе с рекламой Zapret DNS.
|
||
auto, promo = page.grid.tile_rect(AUTO_CHOICE), page.grid.tile_rect(PROMO_TILE_KEY)
|
||
self.assertFalse(auto.isEmpty() or promo.isEmpty())
|
||
self.assertEqual(rect, auto.united(promo))
|
||
self.assertIs(page.onboarding_target("now"), page.now_panel)
|
||
self.assertTrue(page.onboarding_open_subpage("custom_dns"))
|
||
self.open_custom_server.assert_called_once_with(None)
|
||
self.assertFalse(page.onboarding_open_subpage("hosts_file"))
|
||
|
||
def test_one_time_provider_dns_advice_waits_until_the_tour_is_over(self) -> None:
|
||
# Во время экскурсии совет всплыл бы поверх неё и пропал зря: он показывается один раз.
|
||
with patch("ui.onboarding.is_onboarding_tour_active", return_value=True):
|
||
page = self._page()
|
||
page._isp_lane.request.assert_not_called()
|
||
page.on_page_activated()
|
||
page._isp_lane.request.assert_not_called()
|
||
|
||
page.on_page_activated()
|
||
page._isp_lane.request.assert_called_once_with()
|
||
page.on_page_activated()
|
||
page._isp_lane.request.assert_called_once_with()
|
||
|
||
@staticmethod
|
||
def _provider_tiles(page):
|
||
"""Плитки серверов без плитки «Автоматически»."""
|
||
return [tile for tile in page.grid.tiles() if tile.kind == "provider" and tile.key != AUTO_CHOICE]
|
||
|
||
# ── построение ──────────────────────────────────────────
|
||
|
||
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(), "Загружаю настройки сети…")
|
||
page._load_lane.request.assert_not_called()
|
||
|
||
def test_first_open_shows_current_dns_without_badge_motion(self) -> None:
|
||
"""Комета и переворот значка — ответ на выбор сервера, а не на открытие страницы."""
|
||
page = self._page(load=False)
|
||
page.show()
|
||
badge = page.now_panel.badge
|
||
self.assertFalse(badge.is_busy())
|
||
|
||
page.on_page_activated()
|
||
|
||
self.assertNotEqual(page.now_panel.title_label.text(), "Загружаю настройки сети…")
|
||
self.assertFalse(badge.is_flipping())
|
||
self.assertFalse(badge.is_busy())
|
||
|
||
page.grid.activated.emit("Quad9")
|
||
|
||
self.assertTrue(badge.is_flipping())
|
||
self.assertTrue(badge.is_busy())
|
||
|
||
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, open_custom_server=Mock(), create_open_url_worker=Mock())
|
||
)
|
||
self.addCleanup(page.deleteLater)
|
||
page._load_lane.request = Mock()
|
||
|
||
page.on_page_activated()
|
||
|
||
page._load_lane.request.assert_called_once_with()
|
||
# Повторное открытие обновляет состояние, а заранее загруженные данные больше не ищет.
|
||
page.on_page_activated()
|
||
self.assertEqual(page._load_lane.request.call_count, 2)
|
||
feature.consume_warmed_page_data.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("Quad9")
|
||
|
||
payload = page._apply_lane.request.call_args.args[0]
|
||
self.assertEqual(payload["action"], "provider")
|
||
self.assertEqual(payload["adapters"], [ETH])
|
||
self.assertEqual(payload["name"], "Quad9")
|
||
self.assertEqual(payload["data"]["ipv4"][0], "9.9.9.9")
|
||
self.assertFalse(payload["ipv6_available"])
|
||
pending = [tile.key for tile in self._provider_tiles(page) if tile.pending]
|
||
self.assertEqual(pending, ["Quad9"])
|
||
self.assertTrue(page.grid.is_charging("Quad9"))
|
||
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="Quad9", adapter_count=1, success_count=1, ipv6=[])
|
||
quad9 = replace(ETHERNET, static_ipv4=("9.9.9.9", "149.112.112.112"), static_ipv6=())
|
||
page._on_apply_done(payload, {"plan": plan, "state": replace(STATE, adapters=(quad9, WIFI_ADAPTER, SPARE_ADAPTER))})
|
||
|
||
self.assertIsNone(page._pending_choice)
|
||
self.assertEqual(page.now_panel.title_label.text(), "Quad9")
|
||
# DNS встал мгновенно: комета сначала замыкает круг, потом значок делает оборот.
|
||
self.assertTrue(page.grid.is_charging("Quad9"))
|
||
page.grid._clock = lambda: 10**9
|
||
page.grid._tick()
|
||
self.assertFalse(page.grid.is_charging())
|
||
self.assertEqual(page.grid.settling_keys(), ["Quad9"])
|
||
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_all_button_checks_every_adapter_and_restores_initial_checks(self) -> None:
|
||
page = self._page()
|
||
panel = page.now_panel
|
||
chips = panel._chips
|
||
|
||
self.assertFalse(panel.all_chip.isHidden())
|
||
self.assertFalse(panel.all_chip.isChecked())
|
||
|
||
panel.all_chip.click()
|
||
self.assertEqual(page._selected_adapters(), [ETH, WIFI, SPARE])
|
||
self.assertTrue(panel.all_chip.isChecked())
|
||
|
||
page.grid.activated.emit("Quad9")
|
||
self.assertEqual(page._apply_lane.request.call_args.args[0]["adapters"], [ETH, WIFI, SPARE])
|
||
|
||
chips[SPARE].setChecked(False)
|
||
self.assertFalse(panel.all_chip.isChecked())
|
||
|
||
chips[SPARE].setChecked(True)
|
||
panel.all_chip.click()
|
||
# Все уже были отмечены — возвращаются исходные отметки: только подключённые.
|
||
self.assertEqual(page._selected_adapters(), [ETH, WIFI])
|
||
self.assertFalse(panel.all_chip.isChecked())
|
||
|
||
def test_all_button_is_hidden_for_a_single_adapter(self) -> None:
|
||
page = self._page(state=replace(STATE, adapters=(ETHERNET,)))
|
||
|
||
self.assertTrue(page.now_panel.all_chip.isHidden())
|
||
|
||
def test_own_recent_check_beats_the_catalog_mark(self) -> None:
|
||
"""Блокировки у провайдеров разные: своя проверка серверов точнее общей отметки каталога."""
|
||
line = LineCheck(
|
||
time="2026-10-12T21:05:00",
|
||
cards=(
|
||
("Google DNS", "ok", ""),
|
||
("Quad9", "network", "подменяет обычные ответы"),
|
||
("AdGuard", "partial", "закрыт DoT"),
|
||
),
|
||
)
|
||
page = self._page(state=replace(STATE, line_check=line))
|
||
tiles = {tile.key: tile for tile in self._provider_tiles(page)}
|
||
|
||
self.assertEqual(tiles["Google DNS"].status, "")
|
||
self.assertIn("Проверка 12.10 21:05 на вашей линии: Работает", tiles["Google DNS"].tooltip)
|
||
self.assertNotIn("В России блокируется", tiles["Google DNS"].tooltip)
|
||
self.assertEqual(tiles["Quad9"].status, "blocked")
|
||
self.assertIn("Блокируется по дороге — подменяет обычные ответы", tiles["Quad9"].tooltip)
|
||
# «Не полностью» отметку каталога не снимает и не ставит, но дату и вывод называет.
|
||
self.assertEqual(tiles["AdGuard"].status, "at_risk")
|
||
self.assertIn("Работает не полностью — закрыт DoT", tiles["AdGuard"].tooltip)
|
||
# Сервера в проверке не было — всё как в каталоге.
|
||
self.assertIn("В России блокируется", tiles["Cloudflare"].tooltip)
|
||
|
||
# Предупреждение после применения — по той же отметке.
|
||
page.grid.activated.emit("Google DNS")
|
||
payload = page._apply_lane.request.call_args.args[0]
|
||
done = page_plans.build_provider_dns_apply_result_plan(name="Google DNS", adapter_count=1, success_count=1, ipv6=[])
|
||
page._on_apply_done(payload, {"plan": done, "state": replace(STATE, line_check=line)})
|
||
self.info_bar.warning.assert_not_called()
|
||
|
||
def test_panel_says_who_really_answers_and_warns_about_interception(self) -> None:
|
||
from dns.answering import Answering
|
||
|
||
page = self._page(state=replace(STATE, adapters=(ETHERNET,)))
|
||
key = page._answering_lane.request.call_args.args[0]
|
||
self.assertEqual(key, ("1.1.1.1", ""))
|
||
|
||
page._on_answering_done(key, Answering(owner="Cloudflare", asn="13335"))
|
||
self.assertIn("Отвечает: Cloudflare (AS13335)", page.now_panel.detail_label.text())
|
||
self.assertEqual(page.now_panel.notice_label.text(), "")
|
||
|
||
page._on_answering_done(key, Answering(owner="Rostelecom", asn="12389", intercepted=True))
|
||
self.assertIn("Rostelecom (AS12389) — не выбранный сервер", page.now_panel.detail_label.text())
|
||
self.assertIn("Вы выбрали Cloudflare, но отвечает не он, а Rostelecom", page.now_panel.notice_label.text())
|
||
|
||
# Замер скорости не стирает предупреждение, найденное этой проверкой.
|
||
page._measure_latency()
|
||
self.assertIn("отвечает не он", page.now_panel.notice_label.text())
|
||
|
||
# Итог помнится: повторный вход не спрашивает сеть заново.
|
||
page._answering_lane.request.reset_mock()
|
||
page._request_answering()
|
||
page._answering_lane.request.assert_not_called()
|
||
|
||
def test_dns_that_stays_beside_the_chosen_server_is_named(self) -> None:
|
||
"""Жалоба 2026-10-13 «шифрованный DNS не спасает»: Windows спрашивает все DNS, что у неё есть."""
|
||
from dns.page_plans import LEFTOVER_ADAPTER, LEFTOVER_IPV6, find_dns_leftovers
|
||
|
||
# У выбранного сервера нет IPv6, а роутер раздаёт свой IPv6-DNS: Windows спрашивает и его.
|
||
leaky = replace(ETHERNET, static_ipv4=("83.220.169.155",), static_ipv6=(), auto_ipv6=("fe80::1", "fec0:0:0:ffff::1"))
|
||
other = replace(WIFI_ADAPTER, auto_ipv4=("192.168.1.1",))
|
||
|
||
found = find_dns_leftovers((leaky, other, SPARE_ADAPTER), [ETH])
|
||
self.assertEqual([(item.kind, item.adapter, item.addresses) for item in found], [
|
||
(LEFTOVER_IPV6, "Ethernet", ("fe80::1",)),
|
||
(LEFTOVER_ADAPTER, "Wi-Fi", ("192.168.1.1",)),
|
||
])
|
||
# Заглушки Windows fec0:… — не DNS; отключённый адаптер не в счёт; пока сервер не выбран, утечки нет.
|
||
only_stub = replace(leaky, auto_ipv6=("fec0:0:0:ffff::1", "fec0:0:0:ffff::2"))
|
||
self.assertEqual(find_dns_leftovers((only_stub, SPARE_ADAPTER), [ETH]), ())
|
||
self.assertEqual(find_dns_leftovers((WIFI_ADAPTER,), [WIFI]), ())
|
||
# Шифрованный DNS с адресом ::1 закрывает IPv6; тот же сервер на втором адаптере — не утечка.
|
||
closed = replace(leaky, static_ipv4=("127.0.0.1",), static_ipv6=("::1",))
|
||
twin = replace(WIFI_ADAPTER, static_ipv4=("127.0.0.1",), auto_ipv4=())
|
||
self.assertEqual(find_dns_leftovers((closed, twin), [ETH]), ())
|
||
|
||
page = self._page(state=replace(STATE, adapters=(leaky, other, SPARE_ADAPTER)))
|
||
page.now_panel._chips[WIFI].setChecked(False)
|
||
notice = page.now_panel.notice_label.text()
|
||
self.assertIn("На адаптере «Ethernet» по IPv6 остался DNS роутера или провайдера (fe80::1)", notice)
|
||
self.assertIn("Адаптер «Wi-Fi» подключён, но не отмечен: на нём свой DNS (192.168.1.1)", notice)
|
||
# Отметили второй адаптер — его строка ушла, осталась только про IPv6.
|
||
page.now_panel._chips[WIFI].setChecked(True)
|
||
self.assertNotIn("не отмечен", page.now_panel.notice_label.text())
|
||
|
||
def test_interception_is_not_blamed_on_automatic_or_encrypted_dns(self) -> None:
|
||
from dns.answering import Answering
|
||
|
||
for state in (
|
||
replace(STATE, adapters=(WIFI_ADAPTER,)),
|
||
replace(STATE, adapters=(ETHERNET,), local_proxy_mode="anonymized"),
|
||
):
|
||
with self.subTest(state=state.adapters[0].name, proxy=state.local_proxy_mode):
|
||
page = self._page(state=state)
|
||
key = page._answering_lane.request.call_args.args[0]
|
||
page._on_answering_done(key, Answering(owner="Rostelecom", asn="12389", intercepted=True))
|
||
self.assertIn("Отвечает: Rostelecom (AS12389)", page.now_panel.detail_label.text())
|
||
self.assertNotIn("не выбранный сервер", page.now_panel.detail_label.text())
|
||
self.assertEqual(page.now_panel.notice_label.text(), "")
|
||
|
||
def test_tiles_carry_status_dnssec_and_group_notes(self) -> None:
|
||
page = self._page()
|
||
tiles = {tile.key: tile for tile in self._provider_tiles(page)}
|
||
|
||
self.assertEqual(tiles["Google DNS"].status, "blocked")
|
||
self.assertEqual(tiles["AdGuard"].status, "at_risk")
|
||
self.assertEqual(tiles["Quad9"].status, "")
|
||
self.assertIn("В России блокируется", tiles["Cloudflare"].tooltip)
|
||
self.assertTrue(tiles["Quad9"].has_dnssec)
|
||
self.assertIn("DNSSEC", tiles["Quad9"].tooltip)
|
||
self.assertFalse(tiles["Яндекс DNS"].has_dnssec)
|
||
|
||
notes = {tile.title: tile.note for tile in page.grid.tiles() if tile.kind == "group"}
|
||
self.assertEqual(notes["Для ИИ"], "серверы сообщества — доверия к ним меньше")
|
||
self.assertEqual(notes["Популярные"], "")
|
||
|
||
# При фильтре заголовка группы нет — пояснение уходит в подсказку плитки.
|
||
page._set_filter("Для ИИ")
|
||
self.assertIn("доверия к ним меньше", self._provider_tiles(page)[0].tooltip)
|
||
|
||
def test_choosing_blocked_server_applies_it_and_warns(self) -> None:
|
||
page = self._page()
|
||
|
||
page.grid.activated.emit("Google DNS")
|
||
|
||
payload = page._apply_lane.request.call_args.args[0]
|
||
self.assertEqual(payload["name"], "Google DNS")
|
||
# О блокировке — не до записи, а когда DNS и правда поставлен.
|
||
self.info_bar.warning.assert_not_called()
|
||
done = page_plans.build_provider_dns_apply_result_plan(name="Google DNS", adapter_count=1, success_count=1, ipv6=[])
|
||
page._on_apply_done(payload, {"plan": done, "state": STATE})
|
||
self.assertIn("Google DNS в России блокируется", self.info_bar.warning.call_args.kwargs["title"])
|
||
|
||
# Записать не удалось ни на одном адаптере: это «не применён», и о блокировке ни слова.
|
||
self.info_bar.reset_mock()
|
||
refused = page_plans.build_provider_dns_apply_result_plan(name="Google DNS", adapter_count=2, success_count=0, ipv6=[])
|
||
page._on_apply_done(payload, {"plan": refused, "state": STATE})
|
||
self.assertEqual(self.info_bar.warning.call_count, 1)
|
||
self.assertEqual(self.info_bar.warning.call_args.kwargs["title"], "DNS не применён")
|
||
|
||
self.info_bar.reset_mock()
|
||
page.grid.activated.emit("AdGuard")
|
||
self.info_bar.warning.assert_not_called()
|
||
|
||
def test_dns_set_without_encryption_is_said_out_loud(self) -> None:
|
||
"""Windows молча откатывается с DoH на обычный DNS — программа говорит это вслух."""
|
||
page = self._page()
|
||
page.grid.activated.emit("AdGuard")
|
||
payload = page._apply_lane.request.call_args.args[0]
|
||
|
||
plain = page_plans.build_provider_dns_apply_result_plan(
|
||
name="AdGuard", adapter_count=1, success_count=1, ipv6=[], doh_closed=True
|
||
)
|
||
page._on_apply_done(payload, {"plan": plain, "state": STATE})
|
||
self.assertEqual(self.info_bar.warning.call_args.kwargs["title"], "DNS поставлен без шифрования")
|
||
|
||
self.info_bar.reset_mock()
|
||
encrypted = page_plans.build_provider_dns_apply_result_plan(name="AdGuard", adapter_count=1, success_count=1, ipv6=[])
|
||
page._on_apply_done(payload, {"plan": encrypted, "state": STATE})
|
||
self.info_bar.warning.assert_not_called()
|
||
|
||
def test_encrypted_only_server_is_applied_where_windows_can_encrypt(self) -> None:
|
||
page = self._page()
|
||
|
||
page.grid.activated.emit("Wikimedia DNS")
|
||
|
||
self.assertEqual(page._apply_lane.request.call_args.args[0]["name"], "Wikimedia DNS")
|
||
self.info_bar.warning.assert_not_called()
|
||
tile = {tile.key: tile for tile in self._provider_tiles(page)}["Wikimedia DNS"]
|
||
self.assertIn("Принимает только шифрованные запросы: Windows 11 спрашивает его по DoH", tile.tooltip)
|
||
|
||
def test_tile_with_encrypted_address_goes_through_the_engine_where_windows_cannot_encrypt(self) -> None:
|
||
"""Плитка одна, путь выбирает сама: в Windows 10 — через встроенный шифрованный DNS."""
|
||
page = self._page(state=replace(STATE, doh_supported=False))
|
||
tiles = {tile.key: tile for tile in self._provider_tiles(page)}
|
||
|
||
# Сервер только с шифрованием раньше в Windows 10 не ставился вовсе.
|
||
page.grid.activated.emit("DNS-AI")
|
||
self.assertEqual(page._apply_lane.request.call_args.args[0]["proxy_mode"], "doh")
|
||
self.info_bar.warning.assert_not_called()
|
||
self.assertNotIn("здесь сервер работать не будет", tiles["DNS-AI"].tooltip)
|
||
|
||
for name in ("Zapret DNS", "Quad9"):
|
||
with self.subTest(name=name):
|
||
page.grid.activated.emit(name)
|
||
self.assertEqual(page._apply_lane.request.call_args.args[0]["proxy_mode"], "doh")
|
||
# Метка шифрования на плитке честная: запросы и правда пойдут шифрованными.
|
||
self.assertTrue(tiles[name].has_doh)
|
||
self.assertIn("через встроенный шифрованный DNS", tiles[name].tooltip)
|
||
|
||
# Плитка-режим остаётся собой, а в Windows 11 обычные плитки шифрует сама система.
|
||
page.grid.activated.emit("DNSCrypt")
|
||
self.assertEqual(page._apply_lane.request.call_args.args[0]["proxy_mode"], "dnscrypt")
|
||
modern = self._page(state=replace(STATE, doh_supported=True))
|
||
modern.grid.activated.emit("Zapret DNS")
|
||
self.assertEqual(modern._apply_lane.request.call_args.args[0]["proxy_mode"], "")
|
||
modern_tile = {tile.key: tile for tile in self._provider_tiles(modern)}["Zapret DNS"]
|
||
self.assertTrue(modern_tile.has_doh)
|
||
self.assertNotIn("через встроенный шифрованный DNS", modern_tile.tooltip)
|
||
|
||
def test_ordinary_tile_is_selected_while_the_engine_serves_its_server(self) -> None:
|
||
local = replace(ETHERNET, static_ipv4=("127.0.0.1",), static_ipv6=("::1",))
|
||
state = replace(
|
||
STATE, adapters=(local, WIFI_ADAPTER, SPARE_ADAPTER), doh_supported=False,
|
||
local_proxy_mode="doh", local_proxy_server="Zapret DNS",
|
||
)
|
||
|
||
page = self._page(state=state)
|
||
page.now_panel._chips[WIFI].setChecked(False)
|
||
|
||
self.assertEqual([tile.key for tile in self._provider_tiles(page) if tile.selected], ["Zapret DNS"])
|
||
|
||
def test_encrypted_dns_tile_is_selected_by_the_running_mode(self) -> None:
|
||
local = replace(ETHERNET, static_ipv4=("127.0.0.1",), static_ipv6=("::1",))
|
||
adapters = (local, WIFI_ADAPTER, SPARE_ADAPTER)
|
||
|
||
page = self._page(state=replace(STATE, adapters=adapters, local_proxy_mode="anonymized"))
|
||
page.now_panel._chips[WIFI].setChecked(False)
|
||
selected = [tile.key for tile in self._provider_tiles(page) if tile.selected]
|
||
self.assertEqual(selected, ["DNSCrypt анонимный"])
|
||
self.assertEqual(page.now_panel.title_label.text(), "DNSCrypt анонимный")
|
||
|
||
# Движок не работает: 127.0.0.1 на адаптере — чужой локальный DNS, а не наш режим.
|
||
page = self._page(state=replace(STATE, adapters=adapters, local_proxy_mode=""))
|
||
page.now_panel._chips[WIFI].setChecked(False)
|
||
self.assertEqual([tile.key for tile in self._provider_tiles(page) if tile.selected], [])
|
||
self.assertEqual(page.now_panel.title_label.text(), "Свой DNS")
|
||
|
||
def test_choosing_encrypted_dns_sends_its_mode_to_the_worker(self) -> None:
|
||
page = self._page()
|
||
|
||
page.grid.activated.emit("DNSCrypt")
|
||
|
||
payload = page._apply_lane.request.call_args.args[0]
|
||
self.assertEqual(payload["data"]["local_proxy"], "dnscrypt")
|
||
self.assertEqual(payload["data"]["ipv4"], ["127.0.0.1"])
|
||
self.assertIn("dnscrypt-proxy", {tile.key: tile for tile in self._provider_tiles(page)}["DNSCrypt"].tooltip)
|
||
|
||
# Движок не ответил: страница показывает причину, DNS не менялся.
|
||
failed = page_plans.NetworkProviderDnsPlan(valid=False, ipv4=[], ipv6=[], log_level="WARNING", log_message="Порт 53 занят")
|
||
page._on_apply_done(payload, {"plan": failed, "state": STATE})
|
||
self.assertEqual(self.info_bar.warning.call_args.kwargs["content"], "Порт 53 занят")
|
||
self.assertIsNone(page._pending_choice)
|
||
|
||
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_automatic_dns_is_the_first_tile_in_every_filter(self) -> None:
|
||
page = self._page(state=replace(STATE, adapters=(WIFI_ADAPTER,)))
|
||
|
||
for key in ("all", "Популярные", "Свои DNS"):
|
||
with self.subTest(filter=key):
|
||
page._set_filter(key)
|
||
first = page.grid.tiles()[0]
|
||
self.assertEqual((first.kind, first.key, first.title), ("provider", AUTO_CHOICE, "Автоматически"))
|
||
self.assertEqual(first.note, "DNS от роутера")
|
||
# На адаптере автоматический DNS: плитка выбрана и показывает адрес роутера.
|
||
self.assertTrue(first.selected)
|
||
self.assertEqual(first.address, "192.168.1.1")
|
||
self.assertIn("DHCP", first.tooltip)
|
||
|
||
def test_automatic_tile_is_not_selected_while_a_server_is_set(self) -> None:
|
||
page = self._page(state=replace(STATE, adapters=(ETHERNET,)))
|
||
|
||
auto = page.grid.tile(AUTO_CHOICE)
|
||
|
||
self.assertFalse(auto.selected)
|
||
self.assertEqual(auto.address, "")
|
||
self.assertFalse(hasattr(page.now_panel, "reset_button"))
|
||
|
||
def test_automatic_tile_resets_dns_at_once_without_a_question(self) -> None:
|
||
page = self._page()
|
||
|
||
page.grid.activated.emit(AUTO_CHOICE)
|
||
|
||
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(), "Применяю…")
|
||
auto = page.grid.tile(AUTO_CHOICE)
|
||
self.assertTrue(auto.pending and auto.selected)
|
||
# Пока DNS возвращается, ни один сервер не выглядит выбранным.
|
||
self.assertFalse(any(tile.selected for tile in self._provider_tiles(page)))
|
||
|
||
# ── фильтр ──────────────────────────────────────────────
|
||
|
||
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["Для ИИ"]) + 1)
|
||
|
||
page._set_filter("Свои DNS")
|
||
self.assertEqual([(tile.kind, tile.key) for tile in page.grid.tiles()], [("provider", AUTO_CHOICE), ("add", "__add__")])
|
||
|
||
# ── реклама Zapret DNS ──────────────────────────────────
|
||
|
||
def test_promo_tile_fills_the_first_row_and_moves_no_server(self) -> None:
|
||
page = self._page()
|
||
grid = page.grid
|
||
# Страница не показана: ширину сетке задаём сами и раскладываем плитки заново.
|
||
grid.resize(1100, 100)
|
||
page._render()
|
||
auto, promo = grid.tile_rect(AUTO_CHOICE), grid.tile_rect(PROMO_TILE_KEY)
|
||
|
||
tile = grid.tiles()[1]
|
||
self.assertEqual((tile.kind, tile.key, tile.title), ("promo", PROMO_TILE_KEY, "Zapret DNS"))
|
||
# Значок и цвет — те же, что у плитки самого сервера в группе «Для ИИ».
|
||
server = DNS_PROVIDERS[PROMO_PROVIDER[0]][PROMO_PROVIDER[1]]
|
||
self.assertEqual((tile.icon_name, tile.color), (server["icon"], server["color"]))
|
||
self.assertEqual(tile.address, "dns.zapret.moe")
|
||
self.assertIn("Для ИИ", tile.tooltip)
|
||
self.assertTrue(tile.icons)
|
||
# Стоит в строке «Автоматически» и занимает всё, что в ней оставалось.
|
||
self.assertEqual(promo.top(), auto.top())
|
||
self.assertEqual(promo.left(), auto.right() + 1 + grid.GAP)
|
||
self.assertEqual(promo.right() + 1, grid.width() - grid.SCROLLBAR_GUTTER)
|
||
self.assertGreater(promo.width(), 2 * auto.width())
|
||
# Ни один сервер не сдвинулся: первая группа начинается там же, где и без рекламы.
|
||
with_promo = [(rect.left(), rect.top()) for rect in grid._rects[2:]]
|
||
grid.set_tiles([item for item in grid.tiles() if item.kind != "promo"])
|
||
self.assertEqual([(rect.left(), rect.top()) for rect in grid._rects[1:]], with_promo)
|
||
|
||
def test_promo_tile_is_shown_only_while_all_groups_are_visible(self) -> None:
|
||
page = self._page()
|
||
|
||
for key in ("Для ИИ", "Популярные", "Свои DNS"):
|
||
with self.subTest(filter=key):
|
||
page._set_filter(key)
|
||
self.assertIsNone(page.grid.tile(PROMO_TILE_KEY))
|
||
|
||
page._set_filter("all")
|
||
self.assertIsNotNone(page.grid.tile(PROMO_TILE_KEY))
|
||
|
||
def test_promo_tile_opens_the_site_in_background_and_changes_no_dns(self) -> None:
|
||
page = self._page()
|
||
|
||
page.grid.promo_clicked.emit()
|
||
|
||
page._site_lane.request.assert_called_once_with(ZAPRET_DNS_SITE_URL)
|
||
page._apply_lane.request.assert_not_called()
|
||
self.assertIsNone(page._pending_choice)
|
||
# Задачу создаёт узкая зависимость страницы: ей передаётся сама ссылка.
|
||
worker = page._site_lane._create_worker(7, ZAPRET_DNS_SITE_URL)
|
||
self.create_open_url_worker.assert_called_once_with(7, url="https://dns.zapret.moe/", parent=page)
|
||
self.assertIs(worker, self.create_open_url_worker.return_value)
|
||
|
||
def test_failed_site_opening_shows_the_address_to_open_by_hand(self) -> None:
|
||
page = self._page()
|
||
|
||
page._site_lane._on_result(ZAPRET_DNS_SITE_URL, SimpleNamespace(ok=True))
|
||
self.info_bar.warning.assert_not_called()
|
||
|
||
page._site_lane._on_result(ZAPRET_DNS_SITE_URL, SimpleNamespace(ok=False, error="нет браузера"))
|
||
page._site_lane._on_error(ZAPRET_DNS_SITE_URL, "поток упал")
|
||
|
||
self.assertEqual(self.info_bar.warning.call_count, 2)
|
||
for call in self.info_bar.warning.call_args_list:
|
||
self.assertEqual(call.kwargs["title"], "Не удалось открыть сайт")
|
||
self.assertIn("https://dns.zapret.moe/", call.kwargs["content"])
|
||
|
||
# ── замер скорости ──────────────────────────────────────
|
||
|
||
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.assertTrue(page.now_panel.measure_button.isEnabled())
|
||
page.now_panel.measure_button.click()
|
||
self.assertEqual(page._latency_lane.request.call_count, 1)
|
||
self.assertNotIn("127.0.0.1", servers)
|
||
unmeasured = {"Wikimedia DNS", "DNS-AI"}
|
||
network_tiles = [
|
||
tile for tile in self._provider_tiles(page) if tile.address != "127.0.0.1" and tile.key not in unmeasured
|
||
]
|
||
self.assertEqual({tile.latency for tile in network_tiles}, {"measuring"})
|
||
# Серверы только с шифрованием на обычный запрос замера не отвечают: их не спрашивают.
|
||
self.assertEqual({tile.latency for tile in self._provider_tiles(page) if tile.key in unmeasured}, {""})
|
||
self.assertNotIn("185.71.138.138", servers)
|
||
self.assertNotIn("192.144.59.14", servers)
|
||
# Режимы шифрованного DNS живут на этом компьютере и не замеряются.
|
||
self.assertEqual({tile.latency for tile in self._provider_tiles(page) if tile.address == "127.0.0.1"}, {""})
|
||
|
||
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_backup_address_is_measured_and_shown_in_tooltip(self) -> None:
|
||
"""Как «Результат 2» в DNS Jumper: запасной адрес может молчать, когда основной отвечает."""
|
||
page = self._page()
|
||
|
||
page.now_panel.measure_button.click()
|
||
servers = page._latency_lane.request.call_args.args[0]
|
||
self.assertIn("8.8.8.8", servers)
|
||
self.assertIn("8.8.4.4", servers)
|
||
self.assertEqual(len(servers), len(set(servers)))
|
||
|
||
page._on_latency_done(
|
||
DnsLatencyReport(results={"8.8.8.8": 12.0, "8.8.4.4": None, "1.1.1.1": 30.0, "1.0.0.1": 3.0})
|
||
)
|
||
|
||
tiles = {tile.key: tile for tile in self._provider_tiles(page)}
|
||
self.assertIn("IPv4: 8.8.8.8 — 12 мс, 8.8.4.4 — нет ответа", tiles["Google DNS"].tooltip)
|
||
# Плитка показывает основной адрес; запасной адрес другого сервера «быстрее всех» не делает.
|
||
self.assertEqual((tiles["Google DNS"].latency, tiles["Google DNS"].latency_ms), ("ok", 12.0))
|
||
self.assertTrue(tiles["Google DNS"].fastest)
|
||
self.assertFalse(tiles["Cloudflare"].fastest)
|
||
self.assertEqual(page.latency_summary.text(), "Быстрее всех: Google DNS — 12 мс")
|
||
# Без замера в подсказке просто адреса.
|
||
self.assertIn("IPv4: 94.140.14.14, 94.140.15.15", tiles["AdGuard"].tooltip)
|
||
|
||
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_servers_come_with_the_page_state(self) -> None:
|
||
page = self._page(state=replace(STATE, custom_servers=(HOME, SECURE)))
|
||
|
||
tiles = {tile.key: tile for tile in self._provider_tiles(page)}
|
||
|
||
self.assertTrue(tiles["Дом"].custom)
|
||
self.assertFalse(tiles["Дом"].has_doh)
|
||
self.assertTrue(tiles["dns.example.com"].has_doh)
|
||
self.assertIn("DoH: https://dns.example.com/dns-query", tiles["dns.example.com"].tooltip)
|
||
self.assertEqual(tiles["dns.example.com"].address, "203.0.113.5")
|
||
|
||
def test_page_without_loaded_state_shows_builtin_servers_only(self) -> None:
|
||
page = self._page(load=False)
|
||
|
||
self.assertFalse(any(tile.custom for tile in self._provider_tiles(page)))
|
||
self.assertIn("Quad9", [tile.key for tile in self._provider_tiles(page)])
|
||
|
||
def test_add_tile_and_edit_open_the_custom_server_page(self) -> None:
|
||
page = self._page(state=replace(STATE, custom_servers=(HOME, SECURE)))
|
||
|
||
page.grid.add_clicked.emit()
|
||
self.open_custom_server.assert_called_once_with(None)
|
||
|
||
self._run_menu(page, "dns.example.com", "Редактировать")
|
||
self.assertEqual(self.open_custom_server.call_args.args[0], SECURE)
|
||
|
||
def test_duplicate_and_delete_run_in_background_and_refresh_tiles(self) -> None:
|
||
page = self._page(state=replace(STATE, custom_servers=(HOME, SECURE)))
|
||
|
||
self._run_menu(page, "Дом", "Создать копию")
|
||
self.assertEqual(page._custom_lane.request.call_args.args[0], {"action": "duplicate", "server_id": "custom-1"})
|
||
self._run_menu(page, "Дом", "Удалить")
|
||
self.assertEqual(page._custom_lane.request.call_args.args[0], {"action": "delete", "server_id": "custom-1"})
|
||
|
||
page._on_custom_servers_changed(CustomServerResult(success=True, servers=(SECURE,)))
|
||
|
||
keys = [tile.key for tile in self._provider_tiles(page)]
|
||
self.assertNotIn("Дом", keys)
|
||
self.assertIn("dns.example.com", keys)
|
||
|
||
def test_custom_lane_worker_gets_action_and_server_id(self) -> None:
|
||
page = self._page()
|
||
|
||
page._custom_lane._create_worker(5, {"action": "delete", "server_id": "custom-1"})
|
||
|
||
page._dns.create_custom_server_worker.assert_called_once_with(
|
||
5, action="delete", server_id="custom-1", parent=page
|
||
)
|
||
|
||
def test_failed_custom_change_is_reported_and_keeps_the_list(self) -> None:
|
||
page = self._page(state=replace(STATE, custom_servers=(HOME,)))
|
||
|
||
page._on_custom_servers_changed(CustomServerResult(success=False, servers=(HOME,), error="база занята"))
|
||
|
||
self.assertEqual(self.info_bar.warning.call_args.kwargs["content"], "база занята")
|
||
self.assertIn("Дом", [tile.key for tile in self._provider_tiles(page)])
|
||
|
||
def test_copy_puts_doh_and_addresses_into_clipboard(self) -> None:
|
||
page = self._page(state=replace(STATE, custom_servers=(HOME, SECURE)))
|
||
|
||
self._run_menu(page, "Дом", "Копировать DNS в буфер обмена")
|
||
self.assertEqual(QApplication.clipboard().text(), "192.168.1.1, fd00::1")
|
||
|
||
self._run_menu(page, "dns.example.com", "Копировать DNS в буфер обмена")
|
||
self.assertEqual(QApplication.clipboard().text(), "https://dns.example.com/dns-query, 203.0.113.5")
|
||
|
||
def test_reopening_the_page_reloads_state_with_new_custom_servers(self) -> None:
|
||
page = self._page()
|
||
page._load_lane.request.assert_not_called()
|
||
page._isp_lane.request.reset_mock()
|
||
|
||
page.on_page_activated()
|
||
page._load_lane.request.assert_called_once_with()
|
||
page._on_page_data(replace(STATE, custom_servers=(SECURE,)))
|
||
|
||
self.assertIn("dns.example.com", [tile.key for tile in self._provider_tiles(page)])
|
||
# Совет про DNS провайдера решается один раз, а не при каждом возврате на страницу.
|
||
page._isp_lane.request.assert_not_called()
|
||
|
||
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.now_panel.flush_button.click()
|
||
# Второе нажатие, пока кэш чистится, новый сброс не запускает.
|
||
page._flush_lane.request.assert_called_once_with()
|
||
self.assertTrue(page.now_panel.flush_button.isEnabled())
|
||
page._on_flush_done(page_plans.build_flush_dns_cache_result_plan(success=True, message=""))
|
||
|
||
self.assertEqual(self.info_bar.success.call_args.kwargs["title"], "Кэш DNS очищен")
|
||
page.now_panel.flush_button.click()
|
||
self.assertEqual(page._flush_lane.request.call_count, 2)
|
||
|
||
def test_flush_button_does_not_scroll_the_page(self) -> None:
|
||
# Раньше кнопка на время сброса выключалась: фокус уходил на сетку
|
||
# плиток, и страница прокручивалась к выбранному серверу.
|
||
page = self._page(state=replace(STATE, adapters=(replace(ETHERNET, static_ipv4=("193.233.112.67",)),)))
|
||
window = QMainWindow()
|
||
self.addCleanup(window.deleteLater)
|
||
window.setCentralWidget(page)
|
||
window.resize(1200, 500)
|
||
window.show()
|
||
window.activateWindow()
|
||
self._app.processEvents()
|
||
bar = page.verticalScrollBar()
|
||
self.assertGreater(bar.maximum(), 300)
|
||
|
||
QTest.mouseClick(page.now_panel.flush_button, Qt.MouseButton.LeftButton)
|
||
self._app.processEvents()
|
||
|
||
page._flush_lane.request.assert_called_once_with()
|
||
self.assertEqual(bar.value(), 0)
|
||
self.assertIs(self._app.focusWidget(), page.now_panel.flush_button)
|
||
window.takeCentralWidget()
|
||
|
||
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, "Automatic")
|
||
promo = page.grid.tile(PROMO_TILE_KEY)
|
||
self.assertEqual((promo.title, promo.badge), ("Zapret DNS", "project server"))
|
||
self.assertIn("without a VPN", promo.note)
|
||
self.assertEqual(page.grid.tiles()[2].title, "Encrypted")
|
||
self.assertEqual(page.now_panel.flush_button.text(), "Flush DNS cache")
|
||
|
||
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._custom_lane,
|
||
page._site_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()
|