zapretgui/tests/test_domain_lookup_page.py
loop-uh 7278a3849b
All checks were successful
Zapret source guards / validate (push) Successful in 11s
Починены три контрактных теста: перечень процессов не в потоке окна, защита от устаревших завершений, страница DNS без urllib
Автозапуск при выключенном автозапуске смотрит, не остался ли движок от прежней версии, в фоновом потоке, а решение возвращается в поток окна; при включённом автозапуске перечень процессов не читается вовсе. Завершения чтения hosts и проверки редактора пропускают устаревшего работника, итог проверки домена идёт через очередь последнего запроса. Имя сайта своего DNS вынесено в config.urls.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 13:12:53 -04:00

589 lines
34 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.

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.QtTest import QTest
from PyQt6.QtWidgets import QApplication
from blockcheck_words import report_with_words, with_words
from blockcheck.ui.page import BlockcheckPage
from dns import domain_lookup as engine
from dns.ui import domain_lookup_cards as lookup_cards
from dns.ui.domain_lookup_page import DomainLookupPage
def _report(**overrides) -> engine.DomainLookupReport:
values = dict(
target="example.com",
kind=engine.KIND_DOMAIN,
primary_ip="93.184.216.34",
answers=engine.annotate_answers(
[
engine.ResolverAnswer(engine.DnsServer("Хороший", "9.9.9.9"), "ok", ipv4=("93.184.216.34",), elapsed_ms=10.0),
engine.ResolverAnswer(engine.DnsServer("Провайдер", "10.0.0.53"), "ok", ipv4=("195.82.146.214",)),
]
),
ping=engine.PingReport(ip="93.184.216.34", sent=4, received=4, min_ms=1.0, avg_ms=2.0, max_ms=3.0),
tcp=engine.TcpReport("93.184.216.34", 443, "ok", 5.0),
network=engine.NetworkInfo(asn="64500", prefix="93.184.216.0/24", owner="EXAMPLE"),
sources=(engine.NeighborSource(engine.SOURCE_THC, engine.SOURCE_OK, names=("a.example", "b.example")),),
finished=True,
elapsed_s=1.5,
)
values.update(overrides)
return engine.DomainLookupReport(**values)
def _names(view) -> list[str]:
"""Подписи всех строк, показанных в виде групп."""
return [row.name for group in view.groups() for row in group.rows]
class LookupCardsTests(unittest.TestCase):
"""Карточки «Проверки домена»: сайт теми же пробами, что в BlockCheck, страница DNS-сервера, значки."""
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
SITE = {
"key": "user:example.com",
"label": "example.com",
"level": "fail",
"kind": "sni",
"headline": "example.com не открывается: блокировка по имени",
"dns_note": "",
"targets": [
{
"host": "example.com", "purpose": "сайт", "main": True, "ok": False, "state": "dpi",
"short": "соединение сброшено", "text": "соединение сброшено (93.184.216.34)",
"cause": "by_name", "cause_text": "Блокировка по имени сайта.", "quic": "ok", "quic_text": "работает",
"dns_state": "ok", "registry": {"listed": True, "name": "", "network": ""},
"protocols": [
{"key": "tls12", "title": "TLS 1.2", "state": "fail", "word": "сброс", "text": "соединение сброшено"},
{"key": "tls13", "title": "TLS 1.3", "state": "fail", "word": "сброс", "text": "соединение сброшено"},
{"key": "chrome", "title": "Как Chrome", "state": "ok", "word": "проходит", "text": "проходит"},
{"key": "http", "title": "HTTP", "state": "info", "word": "переход", "text": "переход на HTTPS"},
],
}
],
}
def test_domain_gets_the_same_site_card_as_in_blockcheck(self) -> None:
from dns import domain_lookup_plans as plans
# Пока сайт не проверен, карточки нет; адрес (не домен) так не проверяется вовсе.
self.assertNotIn("site:user:example.com", [card.key for card in lookup_cards.build_lookup_cards(_report())])
# Запись сайта приходит из проверки уже со словами (итог, дороги, метки) — как в отчёте BlockCheck.
report = _report(site=with_words(self.SITE))
site = lookup_cards.build_lookup_cards(report)[0]
self.assertEqual((site.key, site.title, site.site, site.level), ("site:user:example.com", "example.com", True, "fail"))
self.assertEqual([mark.label for mark in site.marks], ["TLS 1.2", "TLS 1.3", "Chrome", "HTTP", "QUIC", "DNS"])
self.assertEqual([mark.state for mark in site.marks][:3], ["fail", "fail", "ok"])
self.assertIn(("в реестре РКН", "info"), site.tags)
# Текстовый отчёт и запись истории тоже говорят про сайт.
text = plans.build_text_report(report)
self.assertIn("=== Как открывается сайт ===", text)
self.assertIn(" ❌ TLS 1.2: соединение сброшено", text)
self.assertIn(" ❌ Как блокируют: Блокировка по имени сайта.", text)
entry = plans.build_history_entry(report)
self.assertEqual((entry["level"], entry["problems"][0]), ("fail", self.SITE["headline"]))
def test_site_that_could_not_be_checked_is_unknown_in_history_not_ok(self) -> None:
from dns import domain_lookup_plans as plans
honest = engine.annotate_answers(
[engine.ResolverAnswer(engine.DnsServer("Хороший", "9.9.9.9"), "ok", ipv4=("93.184.216.34",), elapsed_ms=10.0)]
)
site = {"level": "unknown", "headline": "Сайт проверить не удалось", "targets": []}
entry = plans.build_history_entry(_report(answers=honest, site=site))
self.assertEqual(entry["level"], "unknown")
def test_dns_server_tile_opens_its_own_page_and_shows_the_service_logo(self) -> None:
from blockcheck.ui.result_cards import ResultDetailView, line_icon, plain_tile
from blockcheck.ui.result_cards_model import Line, Section
answers = engine.annotate_answers(
[engine.ResolverAnswer(engine.DnsServer("Cloudflare", "1.1.1.1"), "ok", ipv4=("93.184.216.34",), ipv6=("2606::1",), cnames=("a.example",), ttl=60, elapsed_ms=38.0)]
)
dns = next(card for card in lookup_cards.build_lookup_cards(_report(answers=answers)) if card.key == lookup_cards.KEY_DNS)
[line] = dns.sections[1].lines
page = line.page
self.assertEqual((page.title, page.level), ("Cloudflare", "ok"))
rows = {item.name: item.text for item in page.sections[0].lines}
self.assertEqual((rows["Адрес сервера"], rows["IPv4"], rows["IPv6"], rows["Время ответа"]), ("1.1.1.1", "93.184.216.34", "2606::1", "38 мс"))
self.assertIn("Способ запроса", rows)
# Плитка — с логотипом сервиса и подсказкой, что её можно открыть.
tile = plain_tile(line)
self.assertEqual((tile.icon, tile.icon_color), ("simple:cloudflare:CF", "#F38020"))
self.assertIn("Нажмите, чтобы открыть подробности", tile.hint)
self.assertEqual(plain_tile(Line("ok", "Системный DNS · 8.8.8.8", "1.2.3.4")).icon_color, "")
view = ResultDetailView()
self.addCleanup(view.deleteLater)
view.resize(900, 600)
view.show()
view.show_card(dns)
grid = view.blocks[1].grid
QTest.mouseClick(grid, Qt.MouseButton.LeftButton, pos=grid.tile_rect(0).center().toPoint())
self.assertEqual(view.card(), page)
self.assertTrue(view.go_back())
self.assertEqual(view.card(), dns)
grid = view.blocks[1].grid
grid.grab()
# У строк сети и пинга — свои значки, фраза без значения остаётся с точкой.
section = Section("Сеть адреса", ())
icons = [line_icon(Line("info", name, "x"), section) for name in ("Владелец сети", "ASN", "Подсеть", "Страна", "Пинг 1.2.3.4", "Подключение к порту 443 (HTTPS)")]
self.assertEqual(icons, ["fa5s.building", "fa5s.project-diagram", "fa5s.network-wired", "fa5s.flag", "fa5s.satellite-dish", "fa5s.plug"])
self.assertEqual(line_icon(Line("info", "Пинг до сервера не дошёл"), section), "")
def test_past_lookup_text_is_saved_to_a_file_and_read_back(self) -> None:
import tempfile
from types import SimpleNamespace as NS
from dns import commands
with tempfile.TemporaryDirectory() as folder, patch("config.runtime_layout.APPLICATION_PATHS", NS(logs_dir=folder)):
path = commands.save_domain_lookup_text("example.com", "строка 1\nстрока 2")
self.assertTrue(path.startswith(folder) and path.endswith(".json"))
self.assertEqual(commands.load_past_domain_lookup(path), "строка 1\nстрока 2")
# Чужой или пропавший файл — пусто, а не ошибка.
foreign = os.path.join(folder, "other.json")
with open(foreign, "w", encoding="utf-8") as stream:
stream.write('{"format": "something/else", "text": ["x"]}')
self.assertEqual((commands.load_past_domain_lookup(foreign), commands.load_past_domain_lookup(""), commands.load_past_domain_lookup(path + "x")), ("", "", ""))
class DomainLookupPageTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def test_tab_runs_lookup_and_shows_results(self) -> None:
# Одна страница на файл: повторное создание ломает общий qconfig при завершении.
worker = SimpleNamespace(stage=Mock(), stop=Mock())
feature = SimpleNamespace(create_domain_lookup_worker=Mock(return_value=worker))
with patch.object(BlockcheckPage, "_request_page_initial_state_load", lambda self: None):
host = BlockcheckPage(
blockcheck_feature=SimpleNamespace(),
dns_feature=feature,
create_strategy_scan_worker=lambda *_args, **_kwargs: None,
)
self.addCleanup(host.deleteLater)
# Вкладка стоит слева от «DNS подмена» и создаётся только при первом открытии.
order = host.TAB_ORDER
self.assertLess(order.index("domain_lookup"), order.index("dns_spoofing"))
self.assertIsNone(host._domain_lookup_tab_page)
host._switch_tab(order.index("domain_lookup"))
page = host._domain_lookup_tab_page
self.assertIsInstance(page, DomainLookupPage)
self.assertFalse(page.isHidden())
self.assertEqual(host._normalize_tab_key("ping"), "domain_lookup")
# Пустой ввод ничего не запускает.
page._lane.request = Mock()
page.start_lookup()
page._lane.request.assert_not_called()
self.assertFalse(page._running)
# Запуск: запрос уходит в дорожку, кнопки переключаются.
page.set_target(" example.com ")
page.external_check.setChecked(False)
page.start_lookup()
page._lane.request.assert_called_once_with({"target": "example.com", "use_external": False})
self.assertTrue(page._running)
self.assertFalse(page.start_button.isEnabled())
self.assertFalse(page.stop_button.isHidden())
page.start_lookup() # повторное нажатие во время проверки ничего не делает
page._lane.request.assert_called_once()
# Фабрика воркера получает параметры и подписывается на промежуточные результаты.
created = page._create_worker(5, {"target": "example.com", "use_external": False})
self.assertIs(created, worker)
feature.create_domain_lookup_worker.assert_called_once_with(5, target="example.com", use_external=False, parent=page)
worker.stage.connect.assert_called_once()
# «Стоп» просит воркер остановиться, а не убивает поток.
page._lane.runtime.worker = worker
page.stop_lookup()
worker.stop.assert_called_once()
page._lane.runtime.worker = None
# Промежуточный результат чужого (устаревшего) запуска не показывается.
page._on_stage(999, _report(target="old.example"))
self.assertIsNone(page._report)
# Промежуточные ответы идут десятками: экран перерисовывается один раз, последним из них.
page._lane.runtime.is_current = lambda *_a, **_k: True
shown: list = []
with patch.object(page, "_show_report", shown.append):
for name in ("a.example", "b.example", "c.example"):
page._on_stage(5, _report(target=name))
self.assertEqual(shown, [])
page._flush_stage()
self.assertEqual([item.target for item in shown], ["c.example"])
# Итог: карточки видны, таблица заполнена, заглушка помечена.
page._show_final_report(_report())
self.assertFalse(page._running)
self.assertTrue(page.start_button.isEnabled())
self.assertTrue(page.report_button.isEnabled())
# Итог — тремя карточками (пути в этом отчёте нет); строк-виджетов на каждый сервер нет.
cards = {card.key: card for card in page.result_cards()}
self.assertEqual(list(cards), [lookup_cards.KEY_PING, lookup_cards.KEY_DNS, lookup_cards.KEY_NEIGHBORS])
self.assertEqual([widget.card.key for widget in page.cards.cards()], list(cards))
self.assertFalse(page.cards.isHidden())
ping = cards[lookup_cards.KEY_PING]
self.assertEqual((ping.title, ping.level, ping.status), ("Пинг и сеть", "ok", "Сервер отвечает"))
self.assertIn(("ASN", "AS64500"), [(line.name, line.text) for line in ping.sections[1].lines])
dns = cards[lookup_cards.KEY_DNS]
self.assertEqual((dns.level, dns.status), ("fail", "Адрес назвали 2 из 2 серверов"))
answers = dns.sections[1]
self.assertTrue(answers.tiles)
self.assertEqual(len(answers.lines), 2)
self.assertIn("заглушка (Ростелеком)", answers.lines[1].text)
self.assertEqual(answers.lines[1].state, "fail")
# На самой карточке — только сервер, ответивший не как все.
self.assertIn(answers.lines[1], dns.lines)
self.assertNotIn(answers.lines[0], dns.lines)
neighbors = cards[lookup_cards.KEY_NEIGHBORS]
self.assertEqual(neighbors.status, "Найдено доменов: 2")
self.assertEqual([line.name for line in neighbors.sections[1].lines], ["a.example", "b.example"])
# Нажатие на карточку открывает её подробности страницей с хлебными крошками.
page.cards.cards()[1].opened.emit(dns)
detail = host._detail_view
self.assertFalse(detail.isHidden())
self.assertTrue(page.isHidden())
self.assertEqual(detail.card(), dns)
summary_block, answers_block = detail.blocks
self.assertEqual([tile.title for tile in answers_block.grid.tiles()], ["Хороший", "Провайдер"])
self.assertEqual(answers_block.rows, [])
# Назад — на ту же вкладку.
host._close_subpage()
self.assertTrue(detail.isHidden())
self.assertFalse(page.isHidden())
# Законченная проверка сразу попадает в «Прошлые проверки» на этой вкладке.
self.assertFalse(page.history_card.isHidden())
[past] = page.history_rows.groups()
self.assertEqual(past.title, "Прошлые проверки")
self.assertTrue(past.rows[0].name.startswith(_report().target))
# Прошлые проверки — плитками одной рисующей сетки, а не строкой-виджетом на запись.
[block] = page.history_rows.blocks()
self.assertEqual(block.grid.tiles()[0].title, _report().target)
# Нажатие на плитку открывает ту проверку страницей: текст — из её файла.
opened = []
page.report_requested.connect(opened.append)
feature.load_past_domain_lookup = Mock(return_value="полный текст")
page._history_runs[-1]["log_file"] = "C:/logs/domain_lookup_1.json"
QTest.mouseClick(block.grid, Qt.MouseButton.LeftButton, pos=block.grid.tile_rect(0).center().toPoint())
feature.load_past_domain_lookup.assert_called_once_with("C:/logs/domain_lookup_1.json")
self.assertEqual((opened[-1].text, opened[-1].title.split(" · ")[0]), ("полный текст", _report().target))
# Файла нет (старая запись) — показываем то, что записано в истории, и говорим об этом.
feature.load_past_domain_lookup = Mock(return_value="")
page._open_past(0)
self.assertIn("не сохранился", opened[-1].text)
self.assertIn(_report().target, opened[-1].text)
page.report_requested.disconnect(opened.append)
host._switch_tab(order.index("domain_lookup"))
# Отчёт с путём и найденным фильтром: карточка видна, в таблице узлов стоит отметка.
from diagnostics.path_trace import FilterFacts, Hop, RouteTrace
from diagnostics.quic_probe import QUIC_BLOCKED_BY_NAME, QuicVerdict
from dns import domain_lookup_plans as lookup_plans
from utils.windows_icmp import HOP_ROUTER, HOP_TARGET
hops = tuple(Hop(ttl, HOP_ROUTER, f"10.0.0.{ttl}", 1.0) for ttl in range(1, 4)) + (Hop(4, HOP_TARGET, "93.184.216.34", 40.0),)
page._show_final_report(
_report(
route=RouteTrace("93.184.216.34", hops, reached=True),
quic=QuicVerdict(QUIC_BLOCKED_BY_NAME, "блокируется по имени сайта"),
filter_facts=FilterFacts(True, True, 3, 3),
)
)
path = next(card for card in page.result_cards() if card.key == lookup_cards.KEY_PATH)
self.assertEqual((path.title, path.level, path.status), ("Путь до сервера", "fail", "Найден фильтр по дороге"))
self.assertIn("Фильтр стоит между узлом 2 (10.0.0.2) и узлом 3 (10.0.0.3).", [line.name for line in path.lines])
road = path.sections[-1]
self.assertTrue(road.tiles)
self.assertEqual([row.name for row in road.lines], ["Узел 1", "Узел 2", lookup_plans.FILTER_MARK, "Узел 3", "Узел 4"])
self.assertEqual((road.lines[2].state, road.lines[-1].state), ("fail", "info"))
self.assertIn("сам сервер", road.lines[-1].text)
page._show_final_report(_report())
self.assertNotIn(lookup_cards.KEY_PATH, [card.key for card in page.result_cards()])
# Проверка адреса (не домена): таблицы DNS нет.
page._show_final_report(_report(kind=engine.KIND_IP, target="93.184.216.34", answers=()))
self.assertNotIn(lookup_cards.KEY_DNS, [card.key for card in page.result_cards()])
# Повторная проверка того же домена: поле соседей очищается и заполняется заново,
# хотя текст совпадает с прошлым.
page.start_lookup()
self.assertEqual((page.result_cards(), page.cards.cards()), ([], []))
self.assertTrue(page.cards.isHidden())
page._show_final_report(_report())
self.assertEqual(len(page.cards.cards()), 3)
# Смена языка переводит подписи и не теряет результат.
host.set_ui_language("en")
self.assertEqual(page.start_button.text(), "Check")
self.assertEqual(host._tabs_pivot.items["domain_lookup"].text(), "Domain Lookup")
self.assertIsNotNone(page._report)
# Закрытие страницы закрывает дорожку: поздние результаты игнорируются.
host.cleanup()
self.assertTrue(page._closed)
page._show_final_report(_report(target="late.example"))
self.assertNotEqual(page._report.target, "late.example")
if __name__ == "__main__":
unittest.main()
class RowsViewReuseTests(unittest.TestCase):
def test_only_changed_groups_are_rebuilt(self) -> None:
from PyQt6.QtWidgets import QApplication
from dns import domain_lookup_plans as plans
from dns.ui.domain_lookup_page import RowsView
_app = QApplication.instance() or QApplication([])
def group(title: str, count: int):
return plans.RowGroup(title, tuple(plans.Row("ok", f"строка {n}", "значение") for n in range(count)))
view = RowsView()
view.show_groups((group("Первая", 3), group("Вторая", 2)))
first, second = list(view._blocks)
# Вторая группа выросла: первая остаётся тем же виджетом, пересобирается только вторая.
view.show_groups((group("Первая", 3), group("Вторая", 5)))
self.assertIs(view._blocks[0], first)
self.assertIsNot(view._blocks[1], second)
self.assertEqual(len(view.groups()[1].rows), 5)
class PastCheckTextTests(unittest.TestCase):
"""Прошлая проверка открывается полным текстом сразу, а не только после перезапуска программы."""
def test_command_returns_the_saved_entry_with_the_path_to_the_full_text(self) -> None:
import tempfile
from types import SimpleNamespace
from dns import commands
with tempfile.TemporaryDirectory() as folder:
with (
patch("dns.domain_lookup.run_domain_lookup", return_value=_report(finished=True)),
patch("dns.commands.build_domain_lookup_servers", return_value=()),
patch("config.runtime_layout.APPLICATION_PATHS", SimpleNamespace(logs_dir=folder)),
patch("settings.store.add_tab_history_run") as add,
):
report = commands.run_domain_lookup("example.com")
entry = report.history_entry
# В настройки и на экран уходит одна и та же запись.
self.assertEqual(add.call_args.args[1], entry)
self.assertTrue(entry["log_file"].startswith(folder))
self.assertIn("example.com", commands.load_past_domain_lookup(entry["log_file"]))
def test_just_finished_check_opens_its_full_text_from_the_history(self) -> None:
from PyQt6.QtWidgets import QApplication
from dns.ui.domain_lookup_page import DomainLookupPage
_app = QApplication.instance() or QApplication([])
feature = Mock()
feature.load_past_domain_lookup = lambda path: "полный текст проверки" if path == "run.json" else ""
page = DomainLookupPage(dns_feature=feature, embedded=True)
self.addCleanup(page.deleteLater)
opened: list = []
page.report_requested.connect(opened.append)
saved = {"kind": "domain", "time": "2026-10-08T16:06:00", "title": "example.com", "level": "ok",
"headline": "Готово", "problems": [], "states": {}, "log_file": "run.json"}
page._show_final_report(_report(finished=True, history_entry=saved))
page._open_past(0)
self.assertEqual(opened[-1].text, "полный текст проверки")
self.assertNotIn("не сохранился", opened[-1].text)
def test_past_check_is_shown_with_the_same_cards_as_a_fresh_one(self) -> None:
import tempfile
from types import SimpleNamespace
from PyQt6.QtWidgets import QApplication
from dns import commands
from dns.ui.domain_lookup_page import DomainLookupPage
_app = QApplication.instance() or QApplication([])
with tempfile.TemporaryDirectory() as folder:
with patch("config.runtime_layout.APPLICATION_PATHS", SimpleNamespace(logs_dir=folder)):
path = commands.save_domain_lookup_text("example.com", "текст", _report(finished=True))
# Отчёт возвращается из файла тем же объектом: карточки по нему получаются те же.
self.assertEqual(commands.load_past_domain_lookup_report(path), _report(finished=True))
feature = Mock()
feature.load_past_domain_lookup_report = commands.load_past_domain_lookup_report
feature.load_past_domain_lookup = commands.load_past_domain_lookup
page = DomainLookupPage(dns_feature=feature, embedded=True)
self.addCleanup(page.deleteLater)
fresh = DomainLookupPage(dns_feature=Mock(), embedded=True)
self.addCleanup(fresh.deleteLater)
fresh._show_report(_report(finished=True))
opened: list = []
page.report_requested.connect(opened.append)
run = {"kind": "domain", "time": "2026-10-08T16:06:00", "title": "example.com", "level": "ok",
"headline": "Готово", "problems": [], "states": {}, "log_file": path}
page.set_history([run, {**run, "log_file": ""}])
# Плитки идут свежими сверху: вторая — запись с файлом.
past: list = []
page.past_opened.connect(lambda *args: past.append(args))
page._open_past(1)
# Прошлая проверка уходит отдельной страницей: те же карточки, что у свежей, и текст для кнопки «Отчёт».
[(title, headline, cards, text)] = past
self.assertEqual(cards, fresh.result_cards())
self.assertTrue(cards)
self.assertTrue(title.startswith("example.com · "))
self.assertIn("Проверка: example.com", text)
self.assertTrue(headline)
# Свежий результат самой вкладки при этом не трогается.
self.assertEqual(page.result_cards(), [])
self.assertEqual(opened, [])
from blockcheck.ui.past_cards_view import PastCardsView
view = PastCardsView()
self.addCleanup(view.deleteLater)
got: list = []
view.text_opened.connect(lambda name, body: got.append((name, body)))
view.card_opened.connect(got.append)
view.resize(1000, 500)
view.show()
view.show_run(title, headline, cards, text, root_title="Проверка домена")
self.assertEqual((view.title_label.text(), view.headline_label.text()), (title, headline))
# Страница отдана одной проверке: карточки показаны раскрытыми — шапка и все разделы, а не превью.
reports = view.reports()
self.assertEqual([report.card() for report in reports], cards)
self.assertTrue(all(report.breadcrumb.isHidden() for report in reports))
self.assertTrue(all(report.blocks for report in reports))
dns = next(report for report in reports if report.card().key == lookup_cards.KEY_DNS)
self.assertIsNotNone(dns.blocks[1].grid)
self.assertEqual(view.breadcrumb.count(), 2)
view.report_button.click()
# Плитка DNS-сервера открывает его страницу у хозяина вкладки, а не внутри встроенного отчёта.
page_of_server = dns.card().sections[1].lines[0].page
grid = dns.blocks[1].grid
QTest.mouseClick(grid, Qt.MouseButton.LeftButton, pos=grid.tile_rect(0).center().toPoint())
self.assertEqual(got, [(f"Отчёт: {title}", text), page_of_server])
self.assertEqual(dns.card().key, lookup_cards.KEY_DNS)
# Без текста кнопка «Отчёт» выключена.
view.show_run(title, headline, cards, "")
self.assertFalse(view.report_button.isEnabled())
# У старой записи отчёта в файле нет: остаётся страница с текстом.
page._open_past(0)
self.assertEqual(len(opened), 1)
class OtherTabsPastChecksTests(unittest.TestCase):
"""«DNS подмена» и «DNS-серверы»: прошлая проверка открывается тем же видом, что у вкладки."""
@classmethod
def setUpClass(cls) -> None:
cls._app = QApplication.instance() or QApplication([])
def test_past_page_hosts_a_tab_view_instead_of_cards(self) -> None:
from PyQt6.QtWidgets import QLabel
from blockcheck.ui.past_cards_view import PastCardsView
view = PastCardsView()
self.addCleanup(view.deleteLater)
view.resize(900, 400)
view.show()
first = QLabel("итог первой проверки")
view.show_content("DNS подмена · 08.10 12:00", "Подмены нет", first, "текст", root_title="DNS подмена")
self.assertIs(view.content(), first)
self.assertIs(first.parentWidget(), view)
self.assertEqual(view.reports(), [])
self.assertEqual((view.title_label.text(), view.headline_label.text()), ("DNS подмена · 08.10 12:00", "Подмены нет"))
self.assertTrue(view.report_button.isEnabled())
# Следующая проверка заменяет вид; страница с карточками возвращает сетку.
second = QLabel("итог второй проверки")
view.show_content("DNS подмена · 08.10 13:00", "", second)
self.assertIs(view.content(), second)
self.assertFalse(view.report_button.isEnabled())
view.show_run("example.com", "Готово", [])
self.assertIsNone(view.content())
self.assertEqual(view.reports(), [])
def test_dns_spoofing_tab_opens_a_saved_check_and_falls_back_to_the_record(self) -> None:
from dns.ui.dns_check_page import DNSCheckPage
saved = {"summary": {}, "domains": {}}
feature = Mock()
feature.load_past_dns_check_report = Mock(side_effect=lambda path: saved if path else None)
feature.load_past_dns_check_text = Mock(return_value="текст проверки")
page = DNSCheckPage(dns_feature=feature, embedded=True)
self.addCleanup(page.deleteLater)
opened, texts = [], []
page.past_view_opened.connect(lambda *args: opened.append(args))
page.report_requested.connect(texts.append)
run = {"kind": "dns", "time": "2026-10-08T12:00:00", "title": "DNS подмена", "level": "ok", "headline": "Подмены нет", "problems": [], "log_file": "C:/logs/dns_check_1.json"}
page.set_history([{**run, "log_file": ""}, run])
# Свежая запись сверху: у неё есть файл — открывается видом вкладки.
page._open_past(0)
[(title, headline, widget, text)] = opened
self.addCleanup(widget.deleteLater)
# Текст для кнопки «Отчёт» — из того же файла проверки.
self.assertEqual(text, "текст проверки")
feature.load_past_dns_check_text.assert_called_once_with("C:/logs/dns_check_1.json")
self.assertTrue(title.startswith("DNS подмена · "))
self.assertEqual(headline, "Подмены нет")
self.assertTrue(hasattr(widget, "summary"))
feature.load_past_dns_check_report.assert_called_with("C:/logs/dns_check_1.json")
# У записи без файла — то, что есть в истории, и честная пометка.
page._open_past(1)
self.assertEqual(len(opened), 1)
self.assertIn("не сохранился", texts[-1].text)
def test_dns_servers_tab_keeps_history_and_opens_a_saved_check(self) -> None:
from dns.server_check import ServerCheckReport
from dns.ui.server_check_page import ServerCheckPage
past = ServerCheckReport(finished=True)
feature = Mock()
feature.load_past_server_check_report = Mock(side_effect=lambda path: past if path else None)
page = ServerCheckPage(dns_feature=feature, embedded=True)
self.addCleanup(page.deleteLater)
self.assertTrue(page.history_card.isHidden())
run = {"kind": "servers", "time": "2026-10-08T12:00:00", "title": "Адресов проверено: 42", "level": "warn", "headline": "DoH закрыт у части серверов", "problems": [], "log_file": "C:/logs/server_check_1.json"}
page.set_history([run])
self.assertFalse(page.history_card.isHidden())
[group] = page.history_rows.groups()
self.assertTrue(group.rows[0].name.startswith("Адресов проверено: 42"))
opened, texts = [], []
page.past_view_opened.connect(lambda *args: opened.append(args))
page.report_requested.connect(texts.append)
page._open_past(0)
[(title, headline, widget, _text)] = opened
self.addCleanup(widget.deleteLater)
self.assertEqual(headline, "DoH закрыт у части серверов")
self.assertTrue(hasattr(widget, "verdict") and hasattr(widget, "cards"))
# Запись без файла — запасной вид текстом.
page.set_history([{**run, "log_file": ""}])
page._open_past(0)
self.assertEqual(len(opened), 1)
self.assertIn("не сохранился", texts[-1].text)