zapretgui/tests/test_custom_dns_server_page.py

615 lines
28 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.

"""Страница «Свой DNS»: шапка с итогом, строки полей, сохранение и хлебные крошки."""
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 Qt
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget
from app.page_names import PageName
from dns.state import CustomServerResult
SECURE = {
"id": "secure",
"name": "Мой шифрованный",
"ipv4": ["203.0.113.5", "203.0.113.6"],
"ipv6": ["2001:db8::5"],
"doh": "https://dns.example.com/dns-query",
}
def page_screen(key: str):
from ui.navigation.history import ScreenState
return ScreenState(key=key)
class CustomDnsServerPageTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def setUp(self) -> None:
patcher = patch("dns.ui.custom_server_page.InfoBar")
self.info_bar = patcher.start()
self.addCleanup(patcher.stop)
def _page(self):
from dns.ui.custom_server_page import CustomDnsServerPage
self.open_dns_page = Mock()
self.feature = SimpleNamespace(create_custom_server_worker=Mock())
page = CustomDnsServerPage(deps=SimpleNamespace(dns_feature=self.feature, open_dns_page=self.open_dns_page))
self.addCleanup(page.deleteLater)
page._save_lane.request = Mock()
return page
@staticmethod
def _type(edit, text: str) -> None:
"""Ввод текста пользователем: setText сам сигнал правки не шлёт."""
edit.setText(text)
edit.textEdited.emit(text)
def _crumbs(self, page) -> list[str]:
return [item.text for item in page.breadcrumb.items]
# ── форма ───────────────────────────────────────────────
def test_new_server_form_is_empty_and_named_in_breadcrumbs(self) -> None:
page = self._page()
self.assertTrue(page.handle_page_command("edit_custom_server", {"server": None}))
self.assertEqual(self._crumbs(page), ["Настройка DNS", "Новый DNS"])
self.assertEqual(
(page.doh_edit.text(), page.addresses_edit.text(), page.name_edit.text()),
("", "", ""),
)
self.assertEqual(page.save_button.text(), "Добавить")
self.assertFalse(page.is_editing())
self.assertFalse(page.handle_page_command("something_else", {}))
def test_existing_server_fills_the_fields(self) -> None:
page = self._page()
page.handle_page_command("edit_custom_server", {"server": SECURE})
self.assertEqual(self._crumbs(page), ["Настройка DNS", "Мой шифрованный"])
self.assertEqual(page.doh_edit.text(), "https://dns.example.com/dns-query")
self.assertEqual(page.addresses_edit.text(), "203.0.113.5 203.0.113.6 2001:db8::5")
self.assertEqual(page.name_edit.text(), "Мой шифрованный")
self.assertEqual(page.save_button.text(), "Сохранить")
def test_opening_another_server_clears_the_previous_error(self) -> None:
page = self._page()
page.open_server(None)
page.save_button.click()
self.assertTrue(page.error_text())
page.open_server(SECURE)
self.assertEqual(page.error_text(), "")
self.assertFalse(page.doh_edit.isError())
def test_header_shows_what_the_typed_text_will_become(self) -> None:
page = self._page()
page.open_server(None)
self.assertEqual((page.eyebrow_label.text(), page.title_text.text()), ("Новый сервер", "Свой DNS"))
self.assertIn("одного из двух достаточно", page.detail_label.text())
self._type(page.doh_edit, "https://dns.example.com/dns-query")
# Одна строка DoH: название и адреса подставятся сами — это видно сразу.
self.assertEqual(page.title_text.text(), "dns.example.com")
self.assertIn("найдёт и проверит сама", page.detail_label.text())
self.assertEqual(page.name_edit.placeholderText(), "dns.example.com")
self.assertEqual(page.addresses_edit.placeholderText(), "Найдутся сами")
self._type(page.addresses_edit, "203.0.113.5 2001:db8::5")
self.assertEqual(page.detail_label.text(), "Шифрованный DNS (DoH) · 203.0.113.5 · 2001:db8::5")
self._type(page.doh_edit, "")
self._type(page.name_edit, "Мой сервер")
self.assertEqual(page.title_text.text(), "Мой сервер")
self.assertEqual(page.detail_label.text(), "Обычный DNS без шифрования · 203.0.113.5 · 2001:db8::5")
self.assertEqual(page.addresses_edit.placeholderText(), "9.9.9.9 149.112.112.112")
self.assertIn("Мой сервер", page.header.accessibleDescription())
def test_half_typed_field_keeps_the_last_clear_summary(self) -> None:
page = self._page()
page.open_server(None)
self._type(page.addresses_edit, "9.9.9.9")
clear = page.detail_label.text()
# «9.9.» — ещё не адрес: шапка не мигает ошибкой, пока человек печатает.
self._type(page.addresses_edit, "9.9.9.9 149.112.")
self.assertEqual(page.detail_label.text(), clear)
self.assertEqual(page.error_text(), "")
self.assertEqual(page.doh_edit.text(), "")
def test_typing_by_hand_is_never_moved_between_fields(self) -> None:
page = self._page()
page.open_server(None)
# По одному знаку — это набор, а не вставка: текст остаётся там, где его пишут.
text = "dns.example.com 9.9.9.9"
for length in range(1, len(text) + 1):
self._type(page.doh_edit, text[:length])
self.assertEqual((page.doh_edit.text(), page.addresses_edit.text()), (text, ""))
def test_pasted_doh_with_addresses_is_spread_over_the_fields(self) -> None:
page = self._page()
page.open_server(None)
# Так выглядит строка из «Копировать DNS в буфер обмена».
self._type(page.doh_edit, "https://dns.example.com/dns-query, 203.0.113.5, 2001:db8::5")
self.assertEqual(page.doh_edit.text(), "https://dns.example.com/dns-query")
self.assertEqual(page.addresses_edit.text(), "203.0.113.5 2001:db8::5")
# Новая форма с нуля: черновик прежней формы «Добавить» не стирает.
page = self._page()
page.open_server(None)
self._type(page.addresses_edit, "dns.example.com/dns-query 203.0.113.5")
self.assertEqual((page.doh_edit.text(), page.addresses_edit.text()), ("dns.example.com/dns-query", "203.0.113.5"))
def test_paste_does_not_overwrite_a_filled_field(self) -> None:
page = self._page()
page.open_server(SECURE)
self._type(page.doh_edit, "https://other.example/dns-query 198.51.100.7")
self.assertEqual(page.addresses_edit.text(), "203.0.113.5 203.0.113.6 2001:db8::5")
self.assertEqual(page.doh_edit.text(), "https://other.example/dns-query 198.51.100.7")
def test_existing_server_is_shown_in_the_header(self) -> None:
page = self._page()
page.open_server(SECURE)
self.assertEqual((page.eyebrow_label.text(), page.title_text.text()), ("Свой сервер", "Мой шифрованный"))
self.assertEqual(
page.detail_label.text(),
"Шифрованный DNS (DoH) · 203.0.113.5 · 203.0.113.6 · 2001:db8::5",
)
def test_page_is_built_from_standard_setting_rows(self) -> None:
from ui.widgets.win11_controls import Win11ControlRow
page = self._page()
for row, edit in (
(page.doh_row, page.doh_edit),
(page.addresses_row, page.addresses_edit),
(page.name_row, page.name_edit),
):
self.assertIsInstance(row, Win11ControlRow)
self.assertIs(edit.parent(), row)
# Строка с пояснением — стандартной высоты, как на остальных страницах настроек.
self.assertEqual(row.height(), 70)
self.assertTrue(row.contentLabel.text())
# Кнопки действия стоят в шапке, рядом с итогом.
self.assertIs(page.save_button.parent(), page.header)
self.assertIs(page.cancel_button.parent(), page.header)
def test_form_keeps_rows_whole_when_a_long_error_wraps_in_a_narrow_window(self) -> None:
page = self._page()
window = QMainWindow()
self.addCleanup(window.deleteLater)
window.setCentralWidget(page)
window.resize(820, 560)
window.show()
page.open_server(None)
page.doh_edit.setText("https://dns.example.com/dns-query")
page.save_button.click()
page._on_saved(
CustomServerResult(
success=False,
field="doh",
error="Сервер dns.example.com найден (203.0.113.5), но на запрос DoH не ответил: сервер молчит. "
"Проверьте адрес DoH; возможно, сервер закрыт на вашей линии.",
)
)
self._app.processEvents()
rows = (page.doh_row, page.addresses_row, page.name_row)
self.assertGreater(page.error_label.height(), 30)
for upper, lower in zip(rows, rows[1:]):
self.assertGreaterEqual(lower.y(), upper.y() + upper.height())
self.assertLessEqual(rows[-1].y() + rows[-1].height(), page.card.height())
window.takeCentralWidget()
# ── сохранение ──────────────────────────────────────────
def test_one_doh_line_is_sent_for_saving_with_address_lookup(self) -> None:
page = self._page()
page.open_server(None)
page.doh_edit.setText("dns.example.com/dns-query")
page.save_button.click()
server = page._save_lane.request.call_args.args[0]
self.assertEqual(server["doh"], "https://dns.example.com/dns-query")
self.assertEqual((server["ipv4"], server["ipv6"], server["name"]), ([], [], "dns.example.com"))
self.assertEqual(page.save_button.text(), "Проверяю…")
self.assertIn("проверяю каждый запросом DoH", page.detail_label.text())
# Пока идёт проверка, вокруг значка бежит комета — как при смене DNS.
self.assertTrue(page.badge.is_busy())
# Повторное нажатие, пока идёт проверка, второй раз не сохраняет.
page.save_button.click()
page._save_lane.request.assert_called_once()
def test_fields_are_locked_not_disabled_while_saving(self) -> None:
page = self._page()
page.open_server(SECURE)
page.save_button.click()
self.assertEqual(page.save_button.text(), "Сохраняю…")
for edit in (page.doh_edit, page.addresses_edit, page.name_edit):
self.assertTrue(edit.isReadOnly())
# Выключенное поле теряет фокус — поля только запираются.
self.assertTrue(edit.isEnabled())
self.assertTrue(page.save_button.isEnabled())
def test_enter_in_a_field_saves(self) -> None:
page = self._page()
page.open_server(None)
page.addresses_edit.setText("9.9.9.9")
page.addresses_edit.returnPressed.emit()
self.assertEqual(page._save_lane.request.call_args.args[0]["ipv4"], ["9.9.9.9"])
def test_wrong_input_is_reported_in_the_header_and_marks_its_field(self) -> None:
page = self._page()
page.open_server(None)
page.doh_edit.setText("https://dns.example.com/dns-query")
page.addresses_edit.setText("9.9.9.9 кот")
page.save_button.click()
page._save_lane.request.assert_not_called()
self.assertIn("кот", page.error_label.text())
# Ошибка встаёт на место строки состояния, её поле подчёркнуто.
self.assertFalse(page.error_label.isHidden())
self.assertTrue(page.detail_label.isHidden())
self.assertTrue(page.addresses_edit.isError())
self.assertFalse(page.doh_edit.isError())
self.assertIn("Ошибка", page.error_label.accessibleName())
self.assertIn("кот", page.header.accessibleDescription())
# Правка поля убирает ошибку и возвращает строку состояния.
page.addresses_edit.setText("9.9.9.9")
page.addresses_edit.textEdited.emit("9.9.9.9")
self.assertTrue(page.error_label.isHidden())
self.assertFalse(page.detail_label.isHidden())
self.assertFalse(page.addresses_edit.isError())
self.assertEqual(page.detail_label.text(), "Шифрованный DNS (DoH) · 9.9.9.9")
def test_empty_form_asks_for_doh_or_addresses(self) -> None:
page = self._page()
page.open_server(None)
page.save_button.click()
self.assertIn("адрес DoH", page.error_label.text())
self.assertTrue(page.doh_edit.isError())
def test_failed_check_shows_the_reason_and_unlocks_the_form(self) -> None:
page = self._page()
page.open_server(None)
page.doh_edit.setText("https://dns.example.com/dns-query")
page.save_button.click()
page._on_saved(CustomServerResult(success=False, error="сервер молчит", field="doh"))
self.assertEqual(page.error_label.text(), "сервер молчит")
self.assertTrue(page.doh_edit.isError())
self.assertFalse(page.doh_edit.isReadOnly())
self.assertFalse(page.badge.is_busy() and page._busy)
self.assertEqual(page.save_button.text(), "Добавить")
self.open_dns_page.assert_not_called()
def test_taken_name_is_reported_under_the_name_field(self) -> None:
page = self._page()
page.open_server(SECURE)
page.save_button.click()
page._on_saved(CustomServerResult(success=False, error="Сервер с названием «Дом» уже есть.", field="name"))
self.assertIn("«Дом»", page.error_label.text())
self.assertTrue(page.name_edit.isError())
def test_saved_server_is_announced_and_page_returns_to_dns(self) -> None:
page = self._page()
page.show()
page.open_server(None)
page.doh_edit.setText("https://dns.example.com/dns-query")
page.save_button.click()
page._on_saved(CustomServerResult(success=True, servers=(SECURE,), server=SECURE))
shown = self.info_bar.success.call_args.kwargs
self.assertEqual(shown["title"], "Сервер «Мой шифрованный» добавлен")
self.assertIn("203.0.113.5, 203.0.113.6, 2001:db8::5", shown["content"])
self.open_dns_page.assert_called_once_with()
# Возврат кнопкой «назад» покажет сохранённый сервер: повторное нажатие не создаст второй.
self.assertTrue(page.is_editing())
self.assertEqual(page.addresses_edit.text(), "203.0.113.5 203.0.113.6 2001:db8::5")
self.assertEqual(page.save_button.text(), "Сохранить")
def test_save_finished_after_leaving_the_page_does_not_pull_back_to_dns(self) -> None:
page = self._page()
page.open_server(None)
page.doh_edit.setText("https://dns.example.com/dns-query")
page.save_button.click()
# Человек ушёл на другую страницу, пока искались адреса: страница скрыта.
page.hide()
page._on_saved(CustomServerResult(success=True, servers=(SECURE,), server=SECURE))
self.open_dns_page.assert_not_called()
self.assertTrue(page.is_editing())
# ── журнал экранов: какой сервер открыт ─────────────────
def test_screen_names_the_server_or_the_new_form(self) -> None:
page = self._page()
page.open_server(None)
self.assertEqual(page.navigation_screen().key, "new")
page.open_server(SECURE)
self.assertEqual(page.navigation_screen().key, "server:secure")
self.assertEqual(page.navigation_screen().title, "Мой шифрованный")
def test_back_to_another_server_opens_it_again_by_id(self) -> None:
page = self._page()
page.open_server(None)
other = dict(SECURE, name="Сервер после правки")
self.feature.find_custom_server = Mock(return_value=other)
self.assertTrue(page.restore_navigation_screen(page_screen("server:secure")))
self.feature.find_custom_server.assert_called_once_with("secure")
self.assertEqual(page.name_edit.text(), "Сервер после правки")
self.assertEqual(page.navigation_screen().key, "server:secure")
def test_back_to_a_deleted_server_is_refused(self) -> None:
page = self._page()
page.open_server(None)
self.feature.find_custom_server = Mock(return_value=None)
self.assertFalse(page.restore_navigation_screen(page_screen("server:gone")))
def test_add_tile_keeps_an_unfinished_draft_of_a_new_server(self) -> None:
page = self._page()
page.open_server(None)
page.doh_edit.setText("https://draft.example/dns-query")
page.handle_page_command("edit_custom_server", {"server": None})
self.assertEqual(page.doh_edit.text(), "https://draft.example/dns-query")
def test_add_tile_after_editing_a_server_gives_an_empty_form(self) -> None:
page = self._page()
page.open_server(SECURE)
page.open_server(None)
self.assertEqual((page.doh_edit.text(), page.name_edit.text()), ("", ""))
self.assertFalse(page.is_editing())
def test_back_to_the_new_form_keeps_the_draft(self) -> None:
page = self._page()
page.open_server(None)
page.doh_edit.setText("https://draft.example/dns-query")
self.assertTrue(page.restore_navigation_screen(page_screen("new")))
self.assertEqual(page.doh_edit.text(), "https://draft.example/dns-query")
def test_saving_a_new_server_corrects_the_screen_instead_of_adding_a_step(self) -> None:
page = self._page()
page.open_server(None)
steps, corrections = [], []
page.navigation_screen_changed.connect(lambda: steps.append(page.navigation_screen().key))
page.navigation_screen_corrected.connect(lambda: corrections.append(page.navigation_screen().key))
page.doh_edit.setText("https://dns.example.com/dns-query")
page.save_button.click()
page._on_saved(CustomServerResult(success=True, servers=(SECURE,), server=SECURE))
self.assertEqual(steps, [])
self.assertEqual(corrections, ["server:secure"])
def test_saved_with_a_notice_warns_instead(self) -> None:
page = self._page()
page.open_server(SECURE)
page.save_button.click()
page._on_saved(CustomServerResult(success=True, server=SECURE, notice="Запросы пойдут обычным DNS."))
self.info_bar.success.assert_not_called()
shown = self.info_bar.warning.call_args.kwargs
self.assertEqual((shown["title"], shown["content"]), ("Сервер «Мой шифрованный» сохранён", "Запросы пойдут обычным DNS."))
def test_worker_failure_is_shown_without_a_field(self) -> None:
page = self._page()
page.open_server(SECURE)
page.save_button.click()
page._on_save_failed("база занята")
self.assertIn("база занята", page.error_label.text())
self.assertFalse(page.error_label.isHidden())
self.assertFalse(page.name_edit.isReadOnly())
def test_save_worker_gets_the_server(self) -> None:
page = self._page()
parent_free_lane = page._save_lane
parent_free_lane._create_worker(7, SECURE)
self.feature.create_custom_server_worker.assert_called_once_with(7, action="save", server=SECURE, parent=page)
# ── навигация ───────────────────────────────────────────
def test_cancel_and_breadcrumb_return_to_dns_without_saving(self) -> None:
page = self._page()
page.open_server(SECURE)
page.cancel_button.click()
page.breadcrumb.currentItemChanged.emit("dns")
self.assertEqual(self.open_dns_page.call_count, 2)
page._save_lane.request.assert_not_called()
# Клик по крошке обрезает путь — он восстановлен для следующего захода.
self.assertEqual(self._crumbs(page), ["Настройка DNS", "Мой шифрованный"])
def test_cancel_while_checking_stops_the_lookup_and_drops_its_result(self) -> None:
page = self._page()
page.open_server(None)
page.doh_edit.setText("https://dns.example.com/dns-query")
page.save_button.click()
worker = Mock()
page._save_lane.runtime.worker = worker
self.addCleanup(setattr, page._save_lane.runtime, "worker", None)
page.cancel_button.click()
worker.stop.assert_called_once_with()
self.assertFalse(page.doh_edit.isReadOnly())
self.open_dns_page.assert_called_once_with()
# Итог брошенного сохранения приходит позже — он ничего не показывает и никуда не ведёт.
page._on_saved(CustomServerResult(success=True, server=SECURE))
page._on_save_failed("проверка остановлена")
self.info_bar.success.assert_not_called()
self.assertEqual(page.error_text(), "")
self.open_dns_page.assert_called_once_with()
def test_opening_a_server_drops_an_unfinished_save(self) -> None:
page = self._page()
page.open_server(None)
page.doh_edit.setText("https://dns.example.com/dns-query")
page.save_button.click()
page.open_server(SECURE)
page._on_saved(CustomServerResult(success=False, error="сервер молчит", field="doh"))
self.assertEqual(page.error_text(), "")
self.assertEqual(page.save_button.text(), "Сохранить")
def test_cleanup_closes_the_save_lane_and_ignores_late_results(self) -> None:
page = self._page()
page._save_lane.close = Mock()
page.cleanup()
page._on_saved(CustomServerResult(success=True, server=SECURE))
page._save_lane.close.assert_called_once_with()
self.open_dns_page.assert_not_called()
def test_english_interface(self) -> None:
page = self._page()
page.open_server(None)
page.set_ui_language("en")
self.assertEqual(self._crumbs(page)[1], "New DNS")
self.assertEqual((page.doh_row.titleLabel.text(), page.save_button.text()), ("DoH address", "Add"))
self.assertEqual(page.eyebrow_label.text(), "New server")
self.assertIn("DoH address", page.detail_label.text())
# ── доступность ─────────────────────────────────────────
def test_fields_are_named_and_clear_buttons_stay_out_of_tab_order(self) -> None:
page = self._page()
page.open_server(SECURE)
for edit, name in (
(page.doh_edit, "Адрес DoH"),
(page.addresses_edit, "IP-адреса"),
(page.name_edit, "Название"),
):
self.assertEqual(edit.accessibleName(), name)
self.assertTrue(edit.accessibleDescription())
buttons = [child for child in edit.findChildren(QWidget) if child is not edit and child.inherits("QAbstractButton")]
self.assertTrue(buttons)
for button in buttons:
self.assertEqual(button.focusPolicy(), Qt.FocusPolicy.NoFocus)
self.assertEqual(page.cancel_button.accessibleName(), "Отмена: вернуться к настройке DNS")
self.assertIn("Настройка DNS > Мой шифрованный", page.breadcrumb.accessibleName())
class CustomDnsServerRouteTests(unittest.TestCase):
def test_page_is_a_hidden_child_of_the_dns_page(self) -> None:
from ui.navigation.schema import PAGE_ROUTE_SPECS, get_breadcrumb_chain
spec = PAGE_ROUTE_SPECS[PageName.NETWORK_CUSTOM_DNS]
self.assertEqual((spec.module_name, spec.class_name), ("dns.ui.custom_server_page", "CustomDnsServerPage"))
self.assertTrue(spec.is_hidden)
self.assertFalse(spec.is_top_level)
self.assertEqual(get_breadcrumb_chain(PageName.NETWORK_CUSTOM_DNS), (PageName.NETWORK, PageName.NETWORK_CUSTOM_DNS))
def test_opening_sends_the_server_to_the_page_and_then_shows_it(self) -> None:
from main import window_page_presenters as presenters
calls = []
with patch.object(
presenters, "send_page_command", side_effect=lambda *args, **kwargs: calls.append(("command", args, kwargs)) or True
), patch.object(presenters, "show_page", side_effect=lambda *args, **kwargs: calls.append(("show", args, kwargs)) or True):
self.assertTrue(presenters.open_custom_dns_server("окно", SECURE))
self.assertEqual(
calls,
[
("command", ("окно", PageName.NETWORK_CUSTOM_DNS, "edit_custom_server", {"server": SECURE}), {"ensure": True}),
("show", ("окно", PageName.NETWORK_CUSTOM_DNS), {"allow_internal": True}),
],
)
def test_page_is_not_shown_when_it_rejects_the_command(self) -> None:
from main import window_page_presenters as presenters
with patch.object(presenters, "send_page_command", return_value=False), patch.object(
presenters, "show_page"
) as show, patch.object(presenters, "log"):
self.assertFalse(presenters.open_custom_dns_server("окно", None))
show.assert_not_called()
def test_page_deps_give_narrow_actions(self) -> None:
from ui.page_deps.system import build_custom_dns_server_page_kwargs, build_network_page_kwargs
show_page = Mock()
feature, opener, external = object(), Mock(), Mock()
custom = build_custom_dns_server_page_kwargs(page_name=PageName.NETWORK_CUSTOM_DNS, dns_feature=feature, show_page=show_page)
custom["deps"].open_dns_page()
network = build_network_page_kwargs(
page_name=PageName.NETWORK,
dns_feature=feature,
external_actions_feature=external,
open_custom_dns_server=opener,
)
show_page.assert_called_once_with(PageName.NETWORK, up=True)
self.assertIs(custom["deps"].dns_feature, feature)
self.assertIs(network["deps"].open_custom_server, opener)
# Странице нужна только задача «открыть ссылку», а не все внешние действия.
self.assertIs(network["deps"].create_open_url_worker, external.create_open_url_worker)
if __name__ == "__main__":
unittest.main()