From 070261e9476caf00da461f093eb4871e6bab8890 Mon Sep 17 00:00:00 2001 From: jze9 Date: Mon, 1 Jun 2026 17:05:07 +0500 Subject: [PATCH] test --- .gitignore | 17 + .../ru.cryptopro.nmcades@cryptopro.ru.xpi | Bin 0 -> 76880 bytes fetch_vendor.py | 121 ++++ test_adversarial.py | 521 +++++++++++++++ test_main.py | 629 ++++++++++++++++++ ...сание формы таблицы для выгрузки с ФИАС.xlsx | Bin 0 -> 6875 bytes Пример перечня для поиска в Фиас.xlsx | Bin 0 -> 10314 bytes 7 files changed, 1288 insertions(+) create mode 100644 .gitignore create mode 100644 extensions/ru.cryptopro.nmcades@cryptopro.ru.xpi create mode 100644 fetch_vendor.py create mode 100644 test_adversarial.py create mode 100644 test_main.py create mode 100644 Описание формы таблицы для выгрузки с ФИАС.xlsx create mode 100644 Пример перечня для поиска в Фиас.xlsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38441df --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Портативный Firefox + geckodriver (большие, скачиваются fetch_vendor.py) +vendor/ + +# Сборка +build/ +dist/ +*.spec +__pycache__/ +*.pyc + +# Результаты прогонов (папки вида ДД.ММ.ГГГГ_ЧЧ-ММ-СС) +[0-9][0-9].[0-9][0-9].[0-9][0-9][0-9][0-9]_*/ + +# Прочее +.pytest_cache/ +.venv/ +python-3.8.10-win32.exe diff --git a/extensions/ru.cryptopro.nmcades@cryptopro.ru.xpi b/extensions/ru.cryptopro.nmcades@cryptopro.ru.xpi new file mode 100644 index 0000000000000000000000000000000000000000..0d09434eafd3e6eca8d9ee0460b607ea7e5e613c GIT binary patch literal 76880 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 Xgba7KKb;o=z!#n&Y+{6aX~2H~9+XGt literal 0 HcmV?d00001 diff --git a/fetch_vendor.py b/fetch_vendor.py new file mode 100644 index 0000000..4a87341 --- /dev/null +++ b/fetch_vendor.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +Скачивает портативный Firefox и geckodriver (Windows) в папку vendor/, +чтобы build.py / build_wine.sh встроили их в .exe. Тогда конечному +пользователю нужно установить только КриптоПро — Firefox и драйвер уже внутри. + +Запуск (на машине сборки, можно на Linux): + python fetch_vendor.py # win64 (по умолчанию) + python fetch_vendor.py --arch win32 + python fetch_vendor.py --lang en-US + +Для распаковки Firefox нужен 7-Zip: + Linux: sudo apt install p7zip-full + Windows: установите 7-Zip и добавьте 7z в PATH +""" +from __future__ import annotations + +import argparse +import io +import json +import shutil +import subprocess +import sys +import urllib.request +import zipfile +from pathlib import Path + +ROOT = Path(__file__).parent +VENDOR = ROOT / "vendor" +GECKO_API = "https://api.github.com/repos/mozilla/geckodriver/releases/latest" + + +def _download(url: str) -> bytes: + print(f" ↓ {url}") + req = urllib.request.Request(url, headers={"User-Agent": "fetch-vendor"}) + with urllib.request.urlopen(req, timeout=120) as resp: + return resp.read() + + +def _which_7z() -> str | None: + for name in ("7z", "7za", "7zr"): + if shutil.which(name): + return name + return None + + +def fetch_geckodriver(arch: str) -> None: + print("geckodriver:") + data = json.loads(_download(GECKO_API).decode("utf-8")) + asset = next( + (a for a in data["assets"] if f"{arch}.zip" in a["name"] and a["name"].endswith(".zip")), + None, + ) + if not asset: + names = [a["name"] for a in data["assets"]] + sys.exit(f" Не найден ассет для {arch}. Доступно: {names}") + + blob = _download(asset["browser_download_url"]) + with zipfile.ZipFile(io.BytesIO(blob)) as z: + member = next(n for n in z.namelist() if n.endswith("geckodriver.exe")) + VENDOR.mkdir(exist_ok=True) + with z.open(member) as src, open(VENDOR / "geckodriver.exe", "wb") as dst: + shutil.copyfileobj(src, dst) + print(f" ✓ {VENDOR / 'geckodriver.exe'} ({data['tag_name']})") + + +def fetch_firefox(arch: str, lang: str) -> None: + print("Firefox (портативный):") + sevenzip = _which_7z() + if not sevenzip: + print( + " ✗ 7-Zip не найден — не могу распаковать установщик Firefox.\n" + " Установите 7-Zip (Linux: sudo apt install p7zip-full), либо вручную\n" + " распакуйте Firefox так, чтобы получилось vendor/firefox/firefox.exe" + ) + return + + os_param = "win64" if arch == "win64" else "win" + url = f"https://download.mozilla.org/?product=firefox-latest&os={os_param}&lang={lang}" + installer = VENDOR / "_firefox_setup.exe" + VENDOR.mkdir(exist_ok=True) + installer.write_bytes(_download(url)) + + extract_dir = VENDOR / "_ff_extract" + if extract_dir.exists(): + shutil.rmtree(extract_dir) + # Установщик Firefox — это 7-Zip SFX; внутри папка core/ с firefox.exe. + subprocess.run( + [sevenzip, "x", "-y", f"-o{extract_dir}", str(installer)], + check=True, stdout=subprocess.DEVNULL, + ) + + core = extract_dir / "core" + if not (core / "firefox.exe").exists(): + sys.exit(f" В установщике не найден core/firefox.exe (распаковано в {extract_dir})") + + target = VENDOR / "firefox" + if target.exists(): + shutil.rmtree(target) + shutil.move(str(core), str(target)) + + installer.unlink(missing_ok=True) + shutil.rmtree(extract_dir, ignore_errors=True) + print(f" ✓ {target / 'firefox.exe'}") + + +def main() -> None: + ap = argparse.ArgumentParser() + ap.add_argument("--arch", choices=("win64", "win32"), default="win64") + ap.add_argument("--lang", default="ru") + args = ap.parse_args() + + print(f"Папка: {VENDOR} (arch={args.arch}, lang={args.lang})\n") + fetch_geckodriver(args.arch) + print() + fetch_firefox(args.arch, args.lang) + print("\nГотово. Теперь соберите: python build.py") + + +if __name__ == "__main__": + main() diff --git a/test_adversarial.py b/test_adversarial.py new file mode 100644 index 0000000..60a290e --- /dev/null +++ b/test_adversarial.py @@ -0,0 +1,521 @@ +""" +Adversarial tests — написаны ДО исправлений. +Часть из них должна упасть и показать реальные баги. +Запуск: python3 -m pytest test_adversarial.py -v +""" +from __future__ import annotations + +import os +from pathlib import Path +from unittest.mock import MagicMock, patch +import pytest +import openpyxl + +import main +from main import _normalize_kn, parse_pdf_extract, rename_extract, process_all, FIASSearcher +from selenium.common.exceptions import WebDriverException + + +FOUND_DATA = { + "full_name": "ул. Тестовая, 1", + "status": "Актуальный", + "obj_type": "Здание", + "fias_id": "aaaa-bbbb-cccc-dddd-eeee", + "pdf_url": None, +} + + +def make_xlsx(tmp_path, rows, headers=("КН", "Вид ОН")) -> str: + p = tmp_path / "in.xlsx" + wb = openpyxl.Workbook() + ws = wb.active + ws.append(list(headers)) + for r in rows: + ws.append(list(r)) + wb.save(str(p)) + return str(p) + + +def get_result_rows(tmp_path): + files = list(Path(tmp_path).rglob("Результаты_ФИАС_*.xlsx")) + assert files + return list(openpyxl.load_workbook(str(files[0])).active.iter_rows(min_row=2, values_only=True)) + + +def run(tmp_path, rows, headers=("КН", "Вид ОН"), searcher=None): + xlsx = make_xlsx(tmp_path, rows, headers) + if searcher is None: + searcher = MagicMock() + searcher.search.return_value = None + with patch("main.FIASSearcher", return_value=searcher): + process_all(xlsx, str(tmp_path), log=lambda _: None) + return get_result_rows(tmp_path) + + +def pdf_mock(text): + page = MagicMock() + page.extract_text.return_value = text + ctx = MagicMock() + ctx.__enter__ = lambda s: ctx + ctx.__exit__ = MagicMock(return_value=False) + ctx.pages = [page] + return ctx + + +# ───────────────────────────────────────────────────────────────────────────── +# rename_extract — path safety +# ───────────────────────────────────────────────────────────────────────────── + +class TestRenameExtractPathSafety: + + def _src(self, tmp_path): + f = tmp_path / "_tmp.pdf" + f.write_bytes(b"%PDF") + return str(f) + + def test_slash_in_kn_does_not_escape_output_dir(self, tmp_path): + """ + Слэш в КН создаёт путь вида output_dir/ГАР_01/01_... — + подпапки не существует → FileNotFoundError. + БАГ: kn.replace(':', '_') не трогает '/'. + """ + result = rename_extract(self._src(tmp_path), "01/01:0001001:1", "25.03.2024", str(tmp_path)) + assert Path(result).parent == tmp_path, ( + f"Файл создан вне output_dir: {Path(result).parent}" + ) + + def test_dotdot_in_kn_does_not_escape_output_dir(self, tmp_path): + """ + КН с '../' может вывести файл за пределы сессионной папки. + """ + result = rename_extract(self._src(tmp_path), "../evil:1:1:1", "25.03.2024", str(tmp_path)) + assert tmp_path in Path(result).parents or Path(result).parent == tmp_path, ( + f"Path traversal: {result}" + ) + + def test_backslash_in_kn_does_not_break_path(self, tmp_path): + """Обратный слэш (Windows-данные в Excel) не должен ломать путь.""" + result = rename_extract(self._src(tmp_path), "77\\01:0001001:1", "25.03.2024", str(tmp_path)) + assert Path(result).exists() + + +# ───────────────────────────────────────────────────────────────────────────── +# _normalize_kn — граничные случаи +# ───────────────────────────────────────────────────────────────────────────── + +class TestNormalizeKnEdge: + + def test_only_colons(self): + """':::' остаётся ':::' — не падает.""" + assert _normalize_kn(":::") == ":::" + + def test_numeric_value_from_excel(self): + """ + Excel иногда хранит числа без двоеточий (авто-формат). + Функция не должна добавлять двоеточия — это не КН. + """ + result = _normalize_kn(770100010001) # int, без двоеточий + assert ":" not in result + assert result == "770100010001" + + def test_float_from_excel(self): + """Число с плавающей точкой из Excel — не должно превращаться в 'None'.""" + result = _normalize_kn(77.0) + assert result != "None" + assert "." in result or result.isdigit() + + def test_empty_string(self): + assert _normalize_kn("") == "" + + def test_newlines_inside_kn_removed(self): + """Перенос строки внутри значения ячейки.""" + assert _normalize_kn("77:01\n:0001001:1") == "77:01:0001001:1" + + +# ───────────────────────────────────────────────────────────────────────────── +# parse_pdf_extract — регрессии и граничные случаи +# ───────────────────────────────────────────────────────────────────────────── + +class TestParsePdfExtractEdge: + + UUID = "12345678-abcd-abcd-abcd-123456789abc" + + def _parse(self, tmp_path, text): + p = str(tmp_path / "t.pdf") + Path(p).touch() + with patch("pdfplumber.open", return_value=pdf_mock(text)): + return parse_pdf_extract(p, log=lambda _: None) + + def test_multiple_uuids_keyword_match_beats_fallback(self, tmp_path): + """ + В тексте два UUID; у второго есть ключевое слово 'номер'. + Должен быть выбран второй, а не первый. + """ + uuid_noise = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + uuid_correct = "11111111-2222-3333-4444-555555555555" + _, num = self._parse(tmp_path, f"Прочее {uuid_noise}\nномер выписки {uuid_correct}") + assert num == uuid_correct, f"Выбран неправильный UUID: {num}" + + def test_short_uuid_not_matched(self, tmp_path): + """UUID с неправильным числом символов не должен матчиться.""" + short = "12345678-abcd-abcd-abcd-12345678" # последняя группа слишком короткая + _, num = self._parse(tmp_path, f"номер {short}") + assert num != short + + def test_page_extract_text_returns_none(self, tmp_path): + """ + pdfplumber может вернуть None для страницы без текста. + Код делает `or ""` — не должен падать. + """ + page = MagicMock() + page.extract_text.return_value = None # явный None + ctx = MagicMock() + ctx.__enter__ = lambda s: ctx + ctx.__exit__ = MagicMock(return_value=False) + ctx.pages = [page] + p = str(tmp_path / "t.pdf") + Path(p).touch() + with patch("pdfplumber.open", return_value=ctx): + date, num = parse_pdf_extract(p, log=lambda _: None) + assert date is None + assert num is None + + def test_date_regex_does_not_validate_calendar(self, tmp_path): + """ + Регулярка проверяет только цифровой формат ДД.ММ.ГГГГ, + а не реальность даты — '99.99.9999' пройдёт. + Тест документирует это поведение (не баг, но нужно знать). + """ + date, _ = self._parse(tmp_path, "дата выписки 99.99.9999") + assert date == "99.99.9999" # regexp не валидирует, принимает любые цифры + + +# ───────────────────────────────────────────────────────────────────────────── +# process_all — нештатные входные данные +# ───────────────────────────────────────────────────────────────────────────── + +class TestProcessAllEdge: + + def test_short_data_rows_no_crash(self, tmp_path): + """ + Строка данных короче заголовка (vid_col существует, но ячейка пустая). + openpyxl дополняет None — не должно быть IndexError. + """ + wb = openpyxl.Workbook() + ws = wb.active + ws.append(["КН", "Вид ОН"]) + ws.append(["77:01:0001001:1"]) # только 1 ячейка, нет vid_on + p = str(tmp_path / "in.xlsx") + wb.save(p) + ms = MagicMock() + ms.search.return_value = None + with patch("main.FIASSearcher", return_value=ms): + process_all(p, str(tmp_path), log=lambda _: None) + rows = get_result_rows(tmp_path) + assert len(rows) == 1 + assert rows[0][1] is None # vid_on должен быть None, не IndexError + + def test_header_at_row5_boundary_found(self, tmp_path): + """Заголовок на строке 5 (граница поиска) — должен найтись.""" + wb = openpyxl.Workbook() + ws = wb.active + for _ in range(4): + ws.append(["мусор"]) + ws.append(["КН"]) # строка 5 + ws.append(["77:01:0001001:1"]) + p = str(tmp_path / "in.xlsx") + wb.save(p) + ms = MagicMock() + ms.search.return_value = FOUND_DATA + with patch("main.FIASSearcher", return_value=ms): + process_all(p, str(tmp_path), log=lambda _: None) + ms.search.assert_called() + + def test_header_at_row6_not_found_logs_error(self, tmp_path): + """Заголовок на строке 6 (за пределами поиска) — должен залогировать ошибку.""" + wb = openpyxl.Workbook() + ws = wb.active + for _ in range(5): + ws.append(["мусор"]) + ws.append(["КН"]) # строка 6 — вне окна поиска + ws.append(["77:01:0001001:1"]) + p = str(tmp_path / "in.xlsx") + wb.save(p) + logs = [] + ms = MagicMock() + with patch("main.FIASSearcher", return_value=ms): + process_all(p, str(tmp_path), log=logs.append) + assert any("не найдена" in l for l in logs) + ms.search.assert_not_called() + + def test_differently_formatted_duplicates_searched_once(self, tmp_path): + """ + '77:01:0001001:1' и '77 : 01 : 0001001 : 1' нормализуются одинаково — + должны считаться дублями и вести к одному поиску (+ retry = max 2 вызовов). + """ + ms = MagicMock() + ms.search.return_value = FOUND_DATA + xlsx = make_xlsx(tmp_path, [ + ("77:01:0001001:1",), + ("77 : 01 : 0001001 : 1",), + ], headers=("КН",)) + with patch("main.FIASSearcher", return_value=ms): + process_all(xlsx, str(tmp_path), log=lambda _: None) + assert ms.search.call_count == 1, ( + f"Ожидался 1 вызов (дубли дедуплицированы), получено {ms.search.call_count}" + ) + + def test_exception_on_first_attempt_not_retried_marks_error(self, tmp_path): + """ + WebDriverException на 1-й попытке НЕ вызывает повтор — + КН сразу пишется как 'ошибка'. + Это ограничение текущей логики (retry работает только для None). + """ + attempts = {"n": 0} + + def boom(kn): + attempts["n"] += 1 + if attempts["n"] == 1: + raise WebDriverException("stale") + return FOUND_DATA # вторая попытка дала бы результат, но не дойдёт + + ms = MagicMock() + ms.search.side_effect = boom + rows = run(tmp_path, [("77:01:0001001:1", None)], searcher=ms) + assert rows[0][2] == "ошибка" + assert ms.search.call_count == 1 # второй шанс не даётся + + def test_kn_with_slash_in_excel_does_not_crash_process_all(self, tmp_path): + """ + Если ячейка Excel содержит КН со слэшем (некорректные данные), + download_pdf не должен завершаться path traversal или крашем. + Ошибка должна быть поймана и записана в Excel. + """ + data_with_pdf = {**FOUND_DATA, "pdf_url": "https://fias.nalog.ru/Export/ExportPdfStatement?id=1"} + ms = MagicMock() + ms.search.return_value = data_with_pdf + ms.download_pdf.return_value = False # скачивание не удалось — не важно + # Главное — не должно быть необработанного исключения + rows = run(tmp_path, [("01/01:0001001:1", None)], searcher=ms) + # Строка должна быть в результатах (найдена, пусть без PDF) + assert len(rows) == 1 + + def test_output_xlsx_created_even_when_no_kns(self, tmp_path): + """ + При пустом списке КН результирующий файл всё равно должен существовать. + До исправления: wb_out.save() вызывался только внутри цикла → файл не создавался. + """ + run(tmp_path, []) + files = list(Path(tmp_path).rglob("Результаты_ФИАС_*.xlsx")) + assert files, "Файл результатов не создан при пустом входном xlsx" + + def test_search_result_fias_id_empty_string_not_crashes(self, tmp_path): + """ + fias_id = '' (сайт вернул пустую ячейку) — не должно падать. + openpyxl хранит пустую строку как пустую ячейку и читает обратно как None — + это поведение библиотеки, не баг кода. + """ + ms = MagicMock() + ms.search.return_value = {**FOUND_DATA, "fias_id": ""} + rows = run(tmp_path, [("77:01:0001001:1", None)], searcher=ms) + # Пустая строка → пустая ячейка → None при чтении (openpyxl behavior) + assert rows[0][2] in ("", None) + # Другие поля должны быть на месте + assert rows[0][4] == "ул. Тестовая, 1" + + def test_vid_on_value_preserved_in_output(self, tmp_path): + """Значение «Вид ОН» из Excel должно попасть в итоговую таблицу.""" + ms = MagicMock() + ms.search.return_value = FOUND_DATA + rows = run(tmp_path, [("77:01:0001001:1", "Здание")], searcher=ms) + assert rows[0][1] == "Здание" + + def test_none_kn_cell_skipped(self, tmp_path): + """Пустые ячейки КН не попадают в список и не ищутся.""" + wb = openpyxl.Workbook() + ws = wb.active + ws.append(["КН"]) + ws.append([None]) + ws.append([""]) + ws.append([" "]) + ws.append(["77:01:0001001:1"]) # единственный валидный + p = str(tmp_path / "in.xlsx") + wb.save(p) + ms = MagicMock() + ms.search.return_value = None + with patch("main.FIASSearcher", return_value=ms): + process_all(p, str(tmp_path), log=lambda _: None) + assert ms.search.call_count <= 2 # только 1 КН × 2 retry + + +# ───────────────────────────────────────────────────────────────────────────── +# FIASSearcher — парсинг результатов, граничные случаи +# ───────────────────────────────────────────────────────────────────────────── + +def cells_of(texts): + return [MagicMock(text=t) for t in texts] + + +def tr(texts, html=""): + row = MagicMock() + row.find_elements.side_effect = lambda by, tag: cells_of(texts) if tag == "td" else [] + row.get_attribute.return_value = html + return row + + +def th_row(): + row = MagicMock() + row.find_elements.return_value = [] + return row + + +def run_search_rows(rows): + """Прогоняет логику парсинга search() с замоканным Selenium.""" + s = FIASSearcher(log=lambda _: None) + driver = MagicMock() + driver.find_elements.side_effect = lambda by, sel: ( + rows if "ResultPagerDataPlaceholder" in sel else [] + ) + driver.find_element.return_value = MagicMock() + s.driver = driver + s.wait = MagicMock() + s.wait.until.return_value = MagicMock() + + with patch("main.WebDriverWait") as wdw, patch("main.time"): + inst = MagicMock() + wdw.return_value = inst + calls = [0] + + def until_se(cond): + calls[0] += 1 + if calls[0] <= 2: + from selenium.common.exceptions import TimeoutException + raise TimeoutException() + return MagicMock() + + inst.until.side_effect = until_se + return s.search("77:01:0001001:1") + + +DATA_CELLS = ["Адрес", "Актуальный", "Здание", "x", "y", "fias-id-here"] + + +class TestFIASSearcherEdge: + + def test_whitespace_only_cells_stripped(self): + """Ячейки с пробелами должны возвращаться как пустые строки после strip().""" + cells = [" Адрес ", " ", "Здание", "x", "y", " fias "] + row = tr(cells) + result = run_search_rows([row]) + assert result is not None + assert result["full_name"] == "Адрес" + assert result["status"] == "" + assert result["fias_id"] == "fias" + + def test_six_cells_exactly_accepted(self): + """Ровно 6 ячеек — граница проверки len(cells) >= 6.""" + row = tr(DATA_CELLS) # ровно 6 + result = run_search_rows([row]) + assert result is not None + + def test_five_cells_returns_none(self): + """5 ячеек — меньше минимума, must return None.""" + row = tr(DATA_CELLS[:5]) + result = run_search_rows([row]) + assert result is None + + def test_many_header_rows_then_data(self): + """Несколько заголовочных строк перед строкой данных — пропускаются все.""" + result = run_search_rows([th_row(), th_row(), th_row(), tr(DATA_CELLS)]) + assert result is not None + assert result["full_name"] == "Адрес" + + def test_pdf_url_with_single_quotes_in_html(self): + """URL в одинарных кавычках — rstrip должен убрать кавычку.""" + html = "href='/Export/ExportPdfStatement?id=42&t=pdf'" + row = tr(DATA_CELLS, html=html) + result = run_search_rows([row]) + assert result["pdf_url"] == "https://fias.nalog.ru/Export/ExportPdfStatement?id=42&t=pdf" + + def test_pdf_url_ampersand_decoded(self): + """& в HTML должен быть раскодирован в & в URL.""" + html = 'href="/Export/ExportPdfStatement?a=1&b=2"' + row = tr(DATA_CELLS, html=html) + result = run_search_rows([row]) + assert "&" not in (result["pdf_url"] or "") + assert "a=1&b=2" in (result["pdf_url"] or "") + + def test_no_rows_returns_none(self): + assert run_search_rows([]) is None + + def test_only_header_rows_returns_none(self): + result = run_search_rows([th_row(), th_row()]) + assert result is None + + +# ───────────────────────────────────────────────────────────────────────────── +# tmp_pdf path safety +# ───────────────────────────────────────────────────────────────────────────── +class TestTmpPdfPathSafety: + + def test_slash_in_kn_tmp_pdf_stays_in_session_dir(self, tmp_path): + """ + КН со слэшем не должен создавать tmp_pdf вне session_dir. + До исправления: f"_tmp_{kn.replace(':', '_')}.pdf" оставлял '/' → path traversal. + """ + data_with_pdf = {**FOUND_DATA, "pdf_url": "https://fias.nalog.ru/Export/ExportPdfStatement?id=1"} + ms = MagicMock() + ms.search.return_value = data_with_pdf + + captured_paths = [] + original_download = ms.download_pdf + + def capture_path(url, path): + captured_paths.append(path) + return False # не скачиваем реально + + ms.download_pdf.side_effect = capture_path + + with patch("main.FIASSearcher", return_value=ms): + process_all( + make_xlsx(tmp_path, [("01/999:0001001:1", None)]), + str(tmp_path), + log=lambda _: None, + ) + + # Хотя бы один путь должен был быть передан, и все они должны быть внутри tmp_path + assert captured_paths, "download_pdf не был вызван" + session_dir = str(tmp_path) + for p in captured_paths: + resolved = str(Path(p).resolve()) + assert resolved.startswith(str(Path(session_dir).resolve())), ( + f"tmp_pdf вышел за пределы session_dir: {p}" + ) + + def test_dotdot_in_kn_tmp_pdf_stays_in_session_dir(self, tmp_path): + """../ в КН не должен выводить tmp_pdf за пределы session_dir.""" + data_with_pdf = {**FOUND_DATA, "pdf_url": "https://fias.nalog.ru/Export/ExportPdfStatement?id=1"} + ms = MagicMock() + ms.search.return_value = data_with_pdf + + captured_paths = [] + + def capture_path(url, path): + captured_paths.append(path) + return False + + ms.download_pdf.side_effect = capture_path + + with patch("main.FIASSearcher", return_value=ms): + process_all( + make_xlsx(tmp_path, [("../evil:0001001:1", None)]), + str(tmp_path), + log=lambda _: None, + ) + + assert captured_paths, "download_pdf не был вызван" + for p in captured_paths: + resolved = str(Path(p).resolve()) + assert resolved.startswith(str(Path(str(tmp_path)).resolve())), ( + f"tmp_pdf вышел за пределы session_dir: {p}" + ) diff --git a/test_main.py b/test_main.py new file mode 100644 index 0000000..52b23ac --- /dev/null +++ b/test_main.py @@ -0,0 +1,629 @@ +""" +Автотесты для main.py. +Запуск: python3 -m pytest test_main.py -v +Браузер и настоящий ФИАС не нужны — всё замокано. +""" +from __future__ import annotations + +import os +import re +from pathlib import Path +from unittest.mock import MagicMock, patch, call +import pytest +import openpyxl + +import main +from main import ( + _normalize_kn, + parse_pdf_extract, + rename_extract, + process_all, + FIASSearcher, +) +from selenium.common.exceptions import TimeoutException, WebDriverException + + +# ───────────────────────────────────────────────────────────────────────────── +# Helpers +# ───────────────────────────────────────────────────────────────────────────── + +def make_xlsx(tmp_path: Path, rows, headers=("КН", "Вид ОН")) -> str: + path = tmp_path / "input.xlsx" + wb = openpyxl.Workbook() + ws = wb.active + ws.append(list(headers)) + for row in rows: + ws.append(list(row)) + wb.save(str(path)) + return str(path) + + +def get_result_rows(output_dir: Path) -> list: + """Читает первый Результаты_ФИАС_*.xlsx из папки сессии (без заголовка).""" + files = list(output_dir.rglob("Результаты_ФИАС_*.xlsx")) + assert files, "Результирующий xlsx не найден" + wb = openpyxl.load_workbook(str(files[0])) + return list(wb.active.iter_rows(min_row=2, values_only=True)) + + +def fake_searcher(search_map: dict | None = None, *, side_effect=None): + """Возвращает мок FIASSearcher с заданными результатами поиска.""" + m = MagicMock() + if side_effect is not None: + m.search.side_effect = side_effect + elif search_map is not None: + m.search.side_effect = lambda kn: search_map.get(kn) + else: + m.search.return_value = None + return m + + +def make_pdf_mock(text: str): + """Мок pdfplumber.open(path) как контекстный менеджер с одной страницей.""" + page = MagicMock() + page.extract_text.return_value = text + ctx = MagicMock() + ctx.__enter__ = lambda s: ctx + ctx.__exit__ = MagicMock(return_value=False) + ctx.pages = [page] + return ctx + + +# ───────────────────────────────────────────────────────────────────────────── +# _normalize_kn +# ───────────────────────────────────────────────────────────────────────────── + +class TestNormalizeKn: + def test_already_clean(self): + assert _normalize_kn("77:01:0001001:1") == "77:01:0001001:1" + + def test_spaces_around_colons(self): + assert _normalize_kn("77 : 01 : 0001001 : 1") == "77:01:0001001:1" + + def test_tabs_around_colons(self): + assert _normalize_kn("77\t:\t01\t:\t0001001\t:\t1") == "77:01:0001001:1" + + def test_mixed_whitespace(self): + assert _normalize_kn(" 77 :01: 0001001:1 ") == "77:01:0001001:1" + + def test_trailing_newline(self): + assert _normalize_kn("77:01:0001001:1\n") == "77:01:0001001:1" + + def test_none_becomes_string_none(self): + # raw=None приходит если ячейка пустая, но это отфильтровывается раньше + assert _normalize_kn("None") == "None" + + +# ───────────────────────────────────────────────────────────────────────────── +# parse_pdf_extract +# ───────────────────────────────────────────────────────────────────────────── + +class TestParsePdfExtract: + UUID = "12345678-abcd-abcd-abcd-123456789abc" + + def _parse(self, tmp_path, text): + path = str(tmp_path / "test.pdf") + Path(path).touch() + with patch("pdfplumber.open", return_value=make_pdf_mock(text)): + return parse_pdf_extract(path, log=lambda _: None) + + def test_date_near_keyword(self, tmp_path): + date, _ = self._parse(tmp_path, "дата выписки 25.03.2024 прочее") + assert date == "25.03.2024" + + def test_date_near_vipiska_keyword(self, tmp_path): + date, _ = self._parse(tmp_path, "выписка от 01.01.2023") + assert date == "01.01.2023" + + def test_date_fallback_first_in_text(self, tmp_path): + date, _ = self._parse(tmp_path, "Документ 11.11.2021 без ключевых слов") + assert date == "11.11.2021" + + def test_date_prefers_keyword_over_first(self, tmp_path): + # "01.01.2000" стоит до ключевого слова, "25.03.2024" — рядом с ним + date, _ = self._parse(tmp_path, "01.01.2000 текст дата документа 25.03.2024") + assert date == "25.03.2024" + + def test_uuid_near_keyword(self, tmp_path): + _, num = self._parse(tmp_path, f"номер идентификатор {self.UUID}") + assert num == self.UUID + + def test_uuid_fallback(self, tmp_path): + _, num = self._parse(tmp_path, f"Сведения {self.UUID} конец") + assert num == self.UUID + + def test_uuid_case_insensitive(self, tmp_path): + uuid_upper = self.UUID.upper() + _, num = self._parse(tmp_path, f"Идентификатор {uuid_upper}") + assert num is not None + assert num.lower() == self.UUID.lower() + + def test_nothing_found(self, tmp_path): + date, num = self._parse(tmp_path, "Текст без дат и UUID") + assert date is None + assert num is None + + def test_both_found(self, tmp_path): + date, num = self._parse(tmp_path, f"дата 31.12.2023 номер {self.UUID}") + assert date == "31.12.2023" + assert num == self.UUID + + def test_corrupt_pdf_returns_none_none(self, tmp_path): + path = str(tmp_path / "bad.pdf") + Path(path).write_bytes(b"garbage") + with patch("pdfplumber.open", side_effect=Exception("corrupt")): + date, num = parse_pdf_extract(path, log=lambda _: None) + assert date is None + assert num is None + + def test_empty_text_returns_none_none(self, tmp_path): + date, num = self._parse(tmp_path, "") + assert date is None + assert num is None + + def test_multipage_text_joined(self, tmp_path): + """Текст со всех страниц объединяется.""" + page1, page2 = MagicMock(), MagicMock() + page1.extract_text.return_value = "стр 1" + page2.extract_text.return_value = f"дата 15.06.2022 номер {self.UUID}" + ctx = MagicMock() + ctx.__enter__ = lambda s: ctx + ctx.__exit__ = MagicMock(return_value=False) + ctx.pages = [page1, page2] + path = str(tmp_path / "multi.pdf") + Path(path).touch() + with patch("pdfplumber.open", return_value=ctx): + date, num = parse_pdf_extract(path, log=lambda _: None) + assert date == "15.06.2022" + assert num == self.UUID + + +# ───────────────────────────────────────────────────────────────────────────── +# rename_extract +# ───────────────────────────────────────────────────────────────────────────── + +class TestRenameExtract: + KN = "77:01:0001001:1" + DATE = "25.03.2024" + EXPECTED_STEM = "ГАР_77_01_0001001_1_25.03.2024" + + def _make_src(self, tmp_path): + src = tmp_path / "_tmp.pdf" + src.write_bytes(b"%PDF-test") + return str(src) + + def test_normal_rename(self, tmp_path): + src = self._make_src(tmp_path) + result = rename_extract(src, self.KN, self.DATE, str(tmp_path)) + assert Path(result).exists() + assert Path(result).name == f"{self.EXPECTED_STEM}.pdf" + assert not Path(src).exists() + + def test_colons_replaced_with_underscores(self, tmp_path): + src = self._make_src(tmp_path) + result = rename_extract(src, self.KN, self.DATE, str(tmp_path)) + assert ":" not in Path(result).name + + def test_conflict_appends_counter(self, tmp_path): + (tmp_path / f"{self.EXPECTED_STEM}.pdf").write_bytes(b"existing") + src = self._make_src(tmp_path) + result = rename_extract(src, self.KN, self.DATE, str(tmp_path)) + assert Path(result).name == f"{self.EXPECTED_STEM}_1.pdf" + + def test_multiple_conflicts_increment(self, tmp_path): + for suffix in ("", "_1", "_2"): + (tmp_path / f"{self.EXPECTED_STEM}{suffix}.pdf").write_bytes(b"x") + src = self._make_src(tmp_path) + result = rename_extract(src, self.KN, self.DATE, str(tmp_path)) + assert Path(result).name == f"{self.EXPECTED_STEM}_3.pdf" + + def test_no_date_uses_today(self, tmp_path): + src = self._make_src(tmp_path) + result = rename_extract(src, self.KN, None, str(tmp_path)) + assert Path(result).exists() + assert re.search(r"\d{2}\.\d{2}\.\d{4}", Path(result).name) + + +# ───────────────────────────────────────────────────────────────────────────── +# process_all — интеграционные тесты с замоканным FIASSearcher +# ───────────────────────────────────────────────────────────────────────────── + +FOUND_DATA = { + "full_name": "ул. Тестовая, 1", + "status": "Актуальный", + "obj_type": "Здание", + "fias_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "pdf_url": None, +} + + +class TestProcessAll: + def _run(self, tmp_path, rows, headers=("КН", "Вид ОН"), *, mock_searcher=None): + xlsx = make_xlsx(tmp_path, rows, headers) + if mock_searcher is None: + mock_searcher = fake_searcher() + with patch("main.FIASSearcher", return_value=mock_searcher): + process_all(xlsx, str(tmp_path), log=lambda _: None) + return get_result_rows(tmp_path) + + # ── столбцы ────────────────────────────────────────────────────────────── + + def test_missing_kn_column_returns_early(self, tmp_path): + xlsx = make_xlsx(tmp_path, [("val",)], headers=("НЕТ",)) + mock_s = fake_searcher() + logs = [] + with patch("main.FIASSearcher", return_value=mock_s): + process_all(xlsx, str(tmp_path), log=logs.append) + mock_s.search.assert_not_called() + assert any("не найдена" in l for l in logs) + + def test_kn_column_case_insensitive(self, tmp_path): + # "Кадастровый номер" (смешанный регистр) + rows = self._run(tmp_path, [("77:01:0001001:1",)], headers=("Кадастровый номер",)) + assert len(rows) == 1 + + # ── нормальные сценарии ─────────────────────────────────────────────────── + + def test_kn_found_writes_correct_row(self, tmp_path): + ms = fake_searcher({"77:01:0001001:1": FOUND_DATA}) + rows = self._run(tmp_path, [("77:01:0001001:1", "Здание")], mock_searcher=ms) + assert rows[0][0] == "77:01:0001001:1" + assert rows[0][3] == "Актуальный" + assert rows[0][4] == "ул. Тестовая, 1" + assert rows[0][2] == "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" + + def test_kn_not_found_writes_absent_row(self, tmp_path): + rows = self._run(tmp_path, [("99:99:9999999:9", None)]) + assert rows[0][2] == "отсутствует" + assert rows[0][3] == "отсутствует" + + def test_multiple_kns_all_processed(self, tmp_path): + ms = fake_searcher({ + "77:01:0001001:1": FOUND_DATA, + "77:01:0001001:2": None, + }) + rows = self._run( + tmp_path, + [("77:01:0001001:1", None), ("77:01:0001001:2", None)], + mock_searcher=ms, + ) + assert len(rows) == 2 + + def test_empty_xlsx_produces_no_data_rows(self, tmp_path): + ms = fake_searcher() + rows = self._run(tmp_path, [], mock_searcher=ms) + assert rows == [] + ms.search.assert_not_called() + # Output file must exist even with zero entries + assert list((tmp_path).rglob("Результаты_ФИАС_*.xlsx")) + + # ── дедупликация ────────────────────────────────────────────────────────── + + def test_duplicate_kn_searched_once(self, tmp_path): + # Each unique KN gets up to 2 attempts (retry logic). + # A duplicate must not add extra attempts beyond that. + ms = fake_searcher({"77:01:0001001:1": FOUND_DATA}) # returns on first try + self._run( + tmp_path, + [("77:01:0001001:1", None), ("77:01:0001001:1", None)], + mock_searcher=ms, + ) + # Exactly 1 call: found on first attempt, duplicate skipped entirely + assert ms.search.call_count == 1 + + def test_duplicate_kn_writes_one_row(self, tmp_path): + ms = fake_searcher() + rows = self._run( + tmp_path, + [("77:01:0001001:1", None), ("77:01:0001001:1", None)], + mock_searcher=ms, + ) + assert len(rows) == 1 + + # ── нормализация КН ─────────────────────────────────────────────────────── + + def test_spaces_in_kn_normalized_before_search(self, tmp_path): + # search() is called with the normalised KN (spaces stripped around colons). + # It may be called twice due to retry logic; assert_any_call covers both. + ms = fake_searcher() + self._run(tmp_path, [("77 : 01 : 0001001 : 1", None)], mock_searcher=ms) + ms.search.assert_any_call("77:01:0001001:1") + # Must never be called with the raw un-normalised form + for c in ms.search.call_args_list: + assert c.args[0] == "77:01:0001001:1" + + def test_normalized_kn_written_to_output(self, tmp_path): + ms = fake_searcher() + rows = self._run(tmp_path, [("77 : 01 : 0001001 : 1", None)], mock_searcher=ms) + assert rows[0][0] == "77:01:0001001:1" + + # ── ошибки при поиске ───────────────────────────────────────────────────── + + def test_webdriver_error_writes_exactly_one_error_row(self, tmp_path): + """Регрессия: двойная запись 'ошибка' + 'отсутствует' для одного КН.""" + ms = fake_searcher(side_effect=WebDriverException("crash")) + rows = self._run(tmp_path, [("77:01:0001001:1", None)], mock_searcher=ms) + assert len(rows) == 1 + assert rows[0][2] == "ошибка" + # Убеждаемся что нет второй строки «отсутствует» + statuses = [r[2] for r in rows] + assert "отсутствует" not in statuses + + def test_generic_exception_writes_exactly_one_error_row(self, tmp_path): + ms = fake_searcher(side_effect=RuntimeError("unexpected")) + rows = self._run(tmp_path, [("77:01:0001001:1", None)], mock_searcher=ms) + assert len(rows) == 1 + assert rows[0][2] == "ошибка" + + def test_error_kn_still_marked_done(self, tmp_path): + """После ошибки дубликат того же КН не должен искаться повторно.""" + ms = fake_searcher(side_effect=WebDriverException("crash")) + self._run( + tmp_path, + [("77:01:0001001:1", None), ("77:01:0001001:1", None)], + mock_searcher=ms, + ) + assert ms.search.call_count == 1 + + # ── повтор поиска ───────────────────────────────────────────────────────── + + def test_retry_on_none_result(self, tmp_path): + """Первая попытка даёт None, вторая — данные: КН должен быть найден.""" + results = [None, FOUND_DATA] + ms = fake_searcher(side_effect=lambda kn: results.pop(0)) + rows = self._run(tmp_path, [("77:01:0001001:1", None)], mock_searcher=ms) + assert rows[0][3] == "Актуальный" + assert ms.search.call_count == 2 + + def test_both_retries_none_marks_absent(self, tmp_path): + ms = fake_searcher(side_effect=lambda kn: None) + rows = self._run(tmp_path, [("77:01:0001001:1", None)], mock_searcher=ms) + assert rows[0][2] == "отсутствует" + assert ms.search.call_count == 2 + + +# ───────────────────────────────────────────────────────────────────────────── +# FIASSearcher — парсинг строк таблицы результатов +# ───────────────────────────────────────────────────────────────────────────── + +def make_cell(text: str) -> MagicMock: + c = MagicMock() + c.text = text + return c + + +def make_tr(cells_text: list[str], inner_html: str = "") -> MagicMock: + cells = [make_cell(t) for t in cells_text] + row = MagicMock() + row.find_elements.side_effect = lambda by, tag: cells if tag == "td" else [] + row.get_attribute.return_value = inner_html + return row + + +def make_th_row() -> MagicMock: + """Заголовочная строка — вместо .""" + row = MagicMock() + row.find_elements.return_value = [] # td → пусто + return row + + +def _call_search_on_rows(rows: list) -> dict | None: + """ + Прогоняет только ту часть search(), которая читает строки из DOM, + полностью обходя WebDriverWait / Selenium ожидания. + """ + searcher = FIASSearcher(log=lambda _: None) + + # Настраиваем мок-драйвер + driver = MagicMock() + driver.find_elements.side_effect = lambda by, sel: ( + rows if "ResultPagerDataPlaceholder" in sel else [] + ) + driver.find_element.return_value = MagicMock() + searcher.driver = driver + + wait = MagicMock() + # wait.until всегда возвращает мок-поле для send_keys + wait.until.return_value = MagicMock() + searcher.wait = wait + + with ( + patch("main.WebDriverWait") as mock_wdw, + patch("main.time") as mock_time, + ): + # WebDriverWait(driver, n).until(...) → всегда успешно (кроме visibility) + instance = MagicMock() + mock_wdw.return_value = instance + + # invisibility → TimeoutException (div остаётся видимым) + # staleness → TimeoutException (обновление на месте) + # visibility → успех (возвращает мок) + call_count = [0] + + def until_side_effect(condition): + call_count[0] += 1 + # Первый вызов — invisibility → TimeoutException + # Второй — staleness → TimeoutException + if call_count[0] <= 2: + raise TimeoutException() + return MagicMock() + + instance.until.side_effect = until_side_effect + + return searcher.search("77:01:0001001:1") + + +class TestFIASSearcherRowParsing: + DATA_CELLS = ["ул. Тестовая, 1", "Актуальный", "Здание", "col3", "col4", + "aaaa-bbbb-cccc-dddd-eeee"] + + def test_normal_data_row_parsed(self): + row = make_tr(self.DATA_CELLS, inner_html="") + result = _call_search_on_rows([row]) + assert result is not None + assert result["full_name"] == "ул. Тестовая, 1" + assert result["status"] == "Актуальный" + assert result["fias_id"] == "aaaa-bbbb-cccc-dddd-eeee" + + def test_header_row_skipped(self): + """Регрессия: ... не должен давать None.""" + header = make_th_row() + data = make_tr(self.DATA_CELLS) + result = _call_search_on_rows([header, data]) + assert result is not None + assert result["full_name"] == "ул. Тестовая, 1" + + def test_only_header_row_returns_none(self): + result = _call_search_on_rows([make_th_row()]) + assert result is None + + def test_no_rows_returns_none(self): + result = _call_search_on_rows([]) + assert result is None + + def test_pdf_url_extracted_from_html(self): + html = 'href="/Export/ExportPdfStatement?id=123&type=abc"' + row = make_tr(self.DATA_CELLS, inner_html=html) + result = _call_search_on_rows([row]) + assert result is not None + assert result["pdf_url"] == "https://fias.nalog.ru/Export/ExportPdfStatement?id=123&type=abc" + + def test_pdf_url_none_when_absent(self): + row = make_tr(self.DATA_CELLS, inner_html="нет ссылки") + result = _call_search_on_rows([row]) + assert result is not None + assert result["pdf_url"] is None + + def test_pdf_url_extracted_from_second_row_when_header_first(self): + """PDF URL берётся из строки данных, а не из rows[0] (был баг).""" + html = 'href="/Export/ExportPdfStatement?id=CORRECT"' + header = make_th_row() + data = make_tr(self.DATA_CELLS, inner_html=html) + result = _call_search_on_rows([header, data]) + assert result is not None + assert "CORRECT" in (result["pdf_url"] or "") + + def test_row_with_less_than_6_cells_skipped(self): + short_row = make_tr(["a", "b", "c"]) # только 3 ячейки + data = make_tr(self.DATA_CELLS) + result = _call_search_on_rows([short_row, data]) + assert result is not None + assert result["full_name"] == "ул. Тестовая, 1" + + +# ───────────────────────────────────────────────────────────────────────────── +# FIASSearcher.download_pdf +# ───────────────────────────────────────────────────────────────────────────── + +class TestDownloadPdf: + PDF_BYTES = b"%PDF-1.4 fake pdf content" + URL = "https://fias.nalog.ru/Export/ExportPdfStatement?id=1" + + def _searcher(self): + s = FIASSearcher(log=lambda _: None) + s.driver = MagicMock() + s.driver.get_cookies.return_value = [{"name": "session", "value": "abc"}] + s.driver.execute_script.return_value = "Mozilla/5.0" + return s + + def _mock_resp(self, content=None, status=200): + resp = MagicMock() + resp.content = content or self.PDF_BYTES + resp.status_code = status + resp.raise_for_status = MagicMock( + side_effect=None if status < 400 else Exception(f"HTTP {status}") + ) + resp.headers = {"Content-Type": "application/pdf"} + return resp + + @patch("main.requests.Session") + def test_success_writes_file(self, mock_session_cls, tmp_path): + mock_session_cls.return_value.get.return_value = self._mock_resp() + s = self._searcher() + dest = str(tmp_path / "out.pdf") + assert s.download_pdf(self.URL, dest) is True + assert Path(dest).read_bytes() == self.PDF_BYTES + + @patch("main.requests.Session") + def test_non_pdf_response_returns_false(self, mock_session_cls, tmp_path): + mock_session_cls.return_value.get.return_value = self._mock_resp(b"login") + s = self._searcher() + assert s.download_pdf(self.URL, str(tmp_path / "out.pdf")) is False + assert not (tmp_path / "out.pdf").exists() + + @patch("main.requests.Session") + def test_http_error_returns_false(self, mock_session_cls, tmp_path): + resp = self._mock_resp(status=403) + resp.raise_for_status.side_effect = Exception("403 Forbidden") + mock_session_cls.return_value.get.return_value = resp + s = self._searcher() + assert s.download_pdf(self.URL, str(tmp_path / "out.pdf")) is False + + @patch("main.requests.Session") + def test_network_exception_returns_false(self, mock_session_cls, tmp_path): + mock_session_cls.return_value.get.side_effect = ConnectionError("timeout") + s = self._searcher() + assert s.download_pdf(self.URL, str(tmp_path / "out.pdf")) is False + + @patch("main.requests.Session") + def test_cookies_transferred_to_session(self, mock_session_cls, tmp_path): + mock_session = MagicMock() + mock_session_cls.return_value = mock_session + mock_session.get.return_value = self._mock_resp() + s = self._searcher() + s.download_pdf(self.URL, str(tmp_path / "out.pdf")) + mock_session.cookies.set.assert_called_with("session", "abc") + + +class TestFIASSearcherSetup: + """Тесты для обработки ошибок при запуске браузера.""" + + @patch("main.webdriver.Firefox") + @patch("main.FirefoxService") + def test_setup_geckodriver_timeout_raises_webdriver_exception(self, mock_service, mock_ff): + """Таймаут при запуске geckodriver должен привести к понятной ошибке.""" + # Симулируем таймаут подключения к geckodriver + mock_ff.side_effect = WebDriverException("HTTPConnectionPool timeout") + + s = FIASSearcher(log=lambda _: None) + with pytest.raises(WebDriverException) as exc_info: + s.setup() + + assert "браузер" in str(exc_info.value).lower() + + @patch("main.WebDriverWait") + @patch("main.webdriver.Firefox") + @patch("main.FirefoxService") + def test_setup_fias_page_unreachable_raises_webdriver_exception(self, mock_service, mock_ff, mock_wdw): + """Таймаут при открытии страницы ФИАС должен привести к понятной ошибке.""" + mock_driver = MagicMock() + mock_ff.return_value = mock_driver + # Симулируем таймаут при driver.get(FIAS_URL) + mock_driver.get.side_effect = WebDriverException("net::ERR_CONNECTION_TIMED_OUT") + + s = FIASSearcher(log=lambda _: None) + with pytest.raises(WebDriverException) as exc_info: + s.setup() + + assert "fias.nalog.ru" in str(exc_info.value).lower() + assert "интернет" in str(exc_info.value).lower() + + @patch("main.WebDriverWait") + @patch("main.webdriver.Firefox") + @patch("main.FirefoxService") + def test_setup_form_not_loading_raises_webdriver_exception(self, mock_service, mock_ff, mock_wdw): + """Если форма поиска не загружается, должна быть понятная ошибка.""" + mock_driver = MagicMock() + mock_ff.return_value = mock_driver + mock_driver.get.return_value = None + + # Симулируем TimeoutException при ожидании элементов формы + mock_wait_instance = MagicMock() + mock_wdw.return_value = mock_wait_instance + mock_wait_instance.until.side_effect = TimeoutException("Element not found") + + s = FIASSearcher(log=lambda _: None) + with pytest.raises(WebDriverException) as exc_info: + s.setup() + + assert "форма" in str(exc_info.value).lower() or "загрузилась" in str(exc_info.value).lower() diff --git a/Описание формы таблицы для выгрузки с ФИАС.xlsx b/Описание формы таблицы для выгрузки с ФИАС.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..631b682b0f95aeecab0875ed23870d9165c0defa GIT binary patch literal 6875 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 literal 0 HcmV?d00001 diff --git a/Пример перечня для поиска в Фиас.xlsx b/Пример перечня для поиска в Фиас.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..0e4a5c527e44acab4a85d8dcc1565ebecc3fcbe2 GIT binary patch literal 10314 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