zapretgui/tests/test_internet_cleanup.py
loop-uh 66ec508bf2
All checks were successful
Zapret source guards / validate (push) Successful in 11s
Windows-сборка / Dev Dev 21.1.7.79 опубликован
Сброс сети Windows: возвращён полный сброс каталога Winsock
У сброса каталога Winsock нет открытой функции Windows, поэтому он снова
выполняется через netsh winsock reset, как в прежнем сбросе. Удаление одних
надстроек (LSP) через WinAPI убрано: полный сброс делает то же самое и вдобавок
восстанавливает повреждённый каталог.

- шаг «сброс Winsock» требует перезагрузки, итог о ней напоминает;
- из обёртки WinAPI удалён ставший ненужным код чтения каталога Winsock;
- окно подтверждения снова называет действие «Сброс Winsock».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:14:29 +03:00

339 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import subprocess
import unittest
from types import SimpleNamespace
from unittest.mock import patch
from windows_features import internet_cleanup
from windows_features.internet_cleanup import CleanupStep, run_internet_cleanup
from windows_features.internet_cleanup_winapi import NetworkWinApiError, SystemProxy
# Настоящий ответ `netsh interface ipv4 reset` исправной русской Windows 10 с правами
# администратора (сокращён): код завершения 1 и один защищённый пункт с отказом.
RESET_OUTPUT = (
"Сброс Пересылка секций - OK!\n"
"Сброс Глобальный - OK!\n"
"Сброс Интерфейс - OK!\n"
"Сброс Маршрут - OK!\n"
"Сброс - OK!\n"
"Сброс - сбой.\n"
"Отказано в доступе.\n"
"\n"
"Сброс - OK!\n"
"Для завершения этого действия требуется перезагрузка.\n"
)
NO_RIGHTS_OUTPUT = "Запрошенная операция требует повышения прав (запустите от имени администратора).\n"
NETSH_COMMANDS = [
("interface", "ipv4", "reset"),
("interface", "ipv6", "reset"),
("interface", "ipv4", "set", "dynamicport", "tcp", "start=10000", "num=30000"),
("winsock", "reset"),
]
# Настоящий ответ `netsh winsock reset` той же Windows: код завершения 0.
WINSOCK_OUTPUT = "\nСброс каталога Winsock выполнен успешно.\nНеобходимо перезагрузить компьютер, чтобы завершить сброс.\n"
class _FakeWindows:
"""Windows для тестов: помнит, что в ней поменяли."""
def __init__(
self,
*,
winhttp_proxy: str = "",
system_proxy: SystemProxy = SystemProxy(enabled=False, server=""),
listening: set[tuple[str, int]] = frozenset(),
fail: set[str] = frozenset(),
netsh: dict[str, tuple[int, str]] | None = None,
) -> None:
self.winhttp_proxy = winhttp_proxy
self.system_proxy = system_proxy
self.listening = set(listening)
self.fail = set(fail)
# Ответ netsh по последнему слову команды до параметров: «reset», «set» или «winsock».
self.netsh = {"reset": (1, RESET_OUTPUT), "set": (0, "ОК.\n"), "winsock": (0, WINSOCK_OUTPUT), **(netsh or {})}
self.netsh_calls: list[tuple[str, ...]] = []
self.calls: list[str] = []
def _call(self, name: str) -> None:
if name in self.fail:
raise NetworkWinApiError("нужны права администратора")
self.calls.append(name)
def run_netsh(self, *args: str) -> tuple[int, str]:
self.netsh_calls.append(args)
return self.netsh["winsock" if args[0] == "winsock" else args[2]]
def flush_resolver_cache(self) -> bool:
self.calls.append("flush_dns")
return "flush_dns" not in self.fail
def flush_neighbor_and_path_caches(self) -> None:
self._call("flush_caches")
def read_winhttp_proxy(self) -> str:
return self.winhttp_proxy
def reset_winhttp_proxy(self) -> None:
self._call("reset_winhttp")
self.winhttp_proxy = ""
def read_system_proxy(self) -> SystemProxy:
return self.system_proxy
def disable_system_proxy(self) -> None:
self._call("disable_system_proxy")
self.system_proxy = SystemProxy(enabled=False, server=self.system_proxy.server)
def accepts_connections(self, host: str, port: int) -> bool:
self.calls.append(f"probe {host}:{port}")
return (host, port) in self.listening
class InternetCleanupTests(unittest.TestCase):
def _run(self, windows: _FakeWindows):
winapi_names = (
"flush_neighbor_and_path_caches",
"read_winhttp_proxy",
"reset_winhttp_proxy",
"read_system_proxy",
"disable_system_proxy",
)
patchers = [
*(patch.object(internet_cleanup.winapi, name, getattr(windows, name)) for name in winapi_names),
patch("dns.winapi.flush_resolver_cache", windows.flush_resolver_cache),
patch.object(internet_cleanup, "_accepts_connections", windows.accepts_connections),
patch.object(internet_cleanup, "_run_netsh", windows.run_netsh),
]
for item in patchers:
item.start()
self.addCleanup(item.stop)
return run_internet_cleanup()
def test_healthy_windows_is_reset_without_false_errors(self) -> None:
# netsh на исправной Windows возвращает код 1 из-за одного защищённого пункта:
# это не ошибка сброса, и «сброшено частично» из-за него показывать нельзя.
windows = _FakeWindows()
result = self._run(windows)
self.assertEqual(windows.netsh_calls, NETSH_COMMANDS)
self.assertEqual(windows.calls, ["flush_dns", "flush_caches"])
self.assertEqual(result.level, "success")
self.assertEqual(result.title, "Сеть Windows сброшена")
self.assertEqual(
result.content,
"TCP/IP IPv4 сброшен. TCP/IP IPv6 сброшен. Динамические TCP-порты: 10000–39999. Winsock сброшен. "
"Кэш DNS очищен. Кэш адресов и маршрутов очищен.\n"
"Менять не пришлось: прокси WinHTTP, системный прокси.\n"
"Перезагрузите Windows, чтобы сброс подействовал.",
)
def test_english_netsh_output_is_understood_too(self) -> None:
output = "Resetting Global, OK!\nResetting Interface, OK!\nResetting , failed.\nAccess is denied.\n\nRestart the computer to complete this action.\n"
self.assertEqual(internet_cleanup._count_reset_items(output), (2, 1))
self.assertEqual(internet_cleanup._count_reset_items(RESET_OUTPUT), (6, 1))
self.assertEqual(internet_cleanup._count_reset_items(NO_RIGHTS_OUTPUT), (0, 0))
def test_tcpip_reset_without_admin_rights_is_reported(self) -> None:
windows = _FakeWindows(netsh={"reset": (1, NO_RIGHTS_OUTPUT)})
result = self._run(windows)
self.assertEqual(result.level, "warning")
self.assertEqual(windows.netsh_calls, NETSH_COMMANDS)
self.assertIn(
"сброс TCP/IP IPv4 — netsh вернул код 1: Запрошенная операция требует повышения прав",
result.content,
)
self.assertIn("сброс TCP/IP IPv6 — netsh вернул код 1", result.content)
self.assertIn("Winsock сброшен.", result.content)
def test_tcpip_reset_refused_in_most_items_is_a_failure(self) -> None:
mostly_refused = "Сброс Глобальный - OK!\nСброс - сбой.\nОтказано в доступе.\nСброс - сбой.\nОтказано в доступе.\n"
windows = _FakeWindows(netsh={"reset": (1, mostly_refused)})
result = self._run(windows)
self.assertEqual(result.level, "warning")
self.assertIn("сброс TCP/IP IPv4 — netsh вернул код 1: Сброс - сбой.", result.content)
def test_dynamic_port_failure_is_reported(self) -> None:
windows = _FakeWindows(netsh={"set": (1, "Параметр задан неверно.\n")})
result = self._run(windows)
self.assertEqual(result.level, "warning")
self.assertIn("динамические TCP-порты — netsh вернул код 1: Параметр задан неверно.", result.content)
self.assertIn("Перезагрузите Windows", result.content)
def test_netsh_is_started_hidden_from_system32_with_windows_encoding(self) -> None:
started: list[tuple[tuple, dict]] = []
def fake_run_hidden(command, **kwargs):
started.append((tuple(command), kwargs))
return SimpleNamespace(returncode=0, stdout="ОК.\n", stderr="")
with (
patch.object(internet_cleanup, "run_hidden", fake_run_hidden),
patch.object(internet_cleanup, "get_system_exe", lambda name: f"C:/Windows/System32/{name}"),
):
result = internet_cleanup._run_netsh("interface", "ipv4", "reset")
self.assertEqual(result, (0, "ОК.\n\n"))
command, kwargs = started[0]
self.assertEqual(command, ("C:/Windows/System32/netsh.exe", "interface", "ipv4", "reset"))
self.assertTrue(kwargs["capture_output"])
self.assertEqual(kwargs["encoding"], internet_cleanup.NETSH_ENCODING)
self.assertGreater(kwargs["timeout"], 0)
self.assertNotIn("shell", kwargs)
def test_hung_netsh_gives_a_readable_error(self) -> None:
def hung(command, **kwargs):
raise subprocess.TimeoutExpired(command, kwargs["timeout"])
with patch.object(internet_cleanup, "run_hidden", hung):
result = run_internet_cleanup([CleanupStep("сброс TCP/IP IPv4", internet_cleanup._reset_tcpip_v4)])
self.assertEqual(result.level, "error")
self.assertEqual(result.content, "Не получилось: сброс TCP/IP IPv4 — netsh не ответил за 30 секунд.")
def test_winhttp_proxy_is_reset_when_set(self) -> None:
windows = _FakeWindows(winhttp_proxy="10.0.0.5:3128")
result = self._run(windows)
self.assertEqual(windows.winhttp_proxy, "")
self.assertIn("Прокси WinHTTP (10.0.0.5:3128) сброшен.", result.content)
def test_dead_local_system_proxy_is_switched_off(self) -> None:
windows = _FakeWindows(
system_proxy=SystemProxy(enabled=True, server="http=127.0.0.1:10809;socks=127.0.0.1:10808"),
)
result = self._run(windows)
self.assertFalse(windows.system_proxy.enabled)
self.assertIn("probe 127.0.0.1:10809", windows.calls)
self.assertIn("probe 127.0.0.1:10808", windows.calls)
self.assertIn("Системный прокси http=127.0.0.1:10809;socks=127.0.0.1:10808 отключён", result.content)
def test_working_or_remote_system_proxy_is_left_alone(self) -> None:
cases = {
"локальный прокси отвечает": _FakeWindows(
system_proxy=SystemProxy(enabled=True, server="127.0.0.1:10809"),
listening={("127.0.0.1", 10809)},
),
"отвечает хотя бы один из адресов": _FakeWindows(
system_proxy=SystemProxy(enabled=True, server="http=localhost:8080;socks=[::1]:1080"),
listening={("::1", 1080)},
),
"прокси в сети": _FakeWindows(system_proxy=SystemProxy(enabled=True, server="proxy.corp.example:3128")),
"часть адресов в сети": _FakeWindows(
system_proxy=SystemProxy(enabled=True, server="http=127.0.0.1:8080;https=10.0.0.5:3128"),
),
"адрес без порта": _FakeWindows(system_proxy=SystemProxy(enabled=True, server="127.0.0.1")),
"прокси выключен": _FakeWindows(system_proxy=SystemProxy(enabled=False, server="127.0.0.1:10809")),
}
for name, windows in cases.items():
with self.subTest(name):
result = self._run(windows)
self.assertNotIn("disable_system_proxy", windows.calls)
self.assertIn("системный прокси", result.content.split("Менять не пришлось:")[1])
def test_remote_proxy_is_never_probed(self) -> None:
windows = _FakeWindows(system_proxy=SystemProxy(enabled=True, server="proxy.corp.example:3128"))
self._run(windows)
self.assertFalse([call for call in windows.calls if call.startswith("probe")])
def test_winsock_reset_failure_is_reported(self) -> None:
windows = _FakeWindows(netsh={"winsock": (1, NO_RIGHTS_OUTPUT)})
result = self._run(windows)
self.assertEqual(result.level, "warning")
self.assertIn("сброс Winsock — netsh вернул код 1: Запрошенная операция требует повышения прав", result.content)
self.assertNotIn("Winsock сброшен", result.content)
def test_failed_step_does_not_stop_the_rest(self) -> None:
windows = _FakeWindows(winhttp_proxy="10.0.0.5:3128", fail={"flush_caches", "reset_winhttp"})
result = self._run(windows)
self.assertEqual(result.level, "warning")
self.assertEqual(result.title, "Сеть сброшена частично")
self.assertIn("Кэш DNS очищен.", result.content)
self.assertIn(
"Не получилось: кэш адресов и маршрутов — нужны права администратора; "
"прокси WinHTTP — нужны права администратора.",
result.content,
)
def test_dns_cache_refusal_is_reported(self) -> None:
result = self._run(_FakeWindows(fail={"flush_dns"}))
self.assertEqual(result.level, "warning")
self.assertIn("кэш DNS — Windows не смогла очистить кэш DNS", result.content)
def test_all_steps_failed_is_an_error(self) -> None:
def broken() -> str:
raise OSError("нет доступа")
result = run_internet_cleanup([CleanupStep("кэш DNS", broken), CleanupStep("сброс Winsock", broken)])
self.assertEqual(result.level, "error")
self.assertEqual(result.title, "Сброс сети не выполнен")
self.assertEqual(result.content, "Не получилось: кэш DNS — нет доступа; сброс Winsock — нет доступа.")
def test_proxy_address_forms(self) -> None:
parse = internet_cleanup._proxy_endpoints
self.assertEqual(parse("127.0.0.1:10809"), [("127.0.0.1", 10809)])
self.assertEqual(parse("http://localhost:8080/"), [("localhost", 8080)])
self.assertEqual(
parse("http=127.0.0.1:1;https=127.0.0.1:1;socks=[::1]:2"),
[("127.0.0.1", 1), ("::1", 2)],
)
for unclear in ("", "127.0.0.1", "127.0.0.1:0", "127.0.0.1:99999", "::1:8080", "host:port"):
with self.subTest(unclear):
self.assertIsNone(parse(unclear))
def test_no_step_needing_reboot_means_no_reboot_reminder(self) -> None:
steps = [
CleanupStep("сброс Winsock", lambda: (_ for _ in ()).throw(OSError("нет доступа")), needs_reboot=True),
CleanupStep("кэш DNS", lambda: "Кэш DNS очищен."),
]
result = run_internet_cleanup(steps)
self.assertEqual(result.level, "warning")
self.assertNotIn("Перезагрузите Windows", result.content)
def test_winapi_wrapper_does_not_start_external_programs(self) -> None:
import inspect
from windows_features import internet_cleanup_winapi
source = inspect.getsource(internet_cleanup_winapi)
self.assertNotIn("subprocess", source)
self.assertNotIn("run_hidden", source)
def test_confirmation_lists_what_is_done_and_warns_about_reboot(self) -> None:
import presets.ui.control.control_runtime as control_runtime
plan = control_runtime.build_internet_cleanup_start_plan(language="ru")
self.assertFalse(plan.blocked)
self.assertEqual(len(plan.confirmations), 1)
content = plan.confirmations[0].content
for expected in ("TCP/IP", "10000–39999", "кэша DNS", "WinHTTP", "системного прокси", "Winsock"):
self.assertIn(expected, content)
self.assertIn("понадобится перезагрузка", content)
if __name__ == "__main__":
unittest.main()