diff --git a/api/src/api/v1/summary.py b/api/src/api/v1/summary.py index 679fd1d..a2ed152 100644 --- a/api/src/api/v1/summary.py +++ b/api/src/api/v1/summary.py @@ -188,10 +188,11 @@ async def get_summary2( "sort_by": sort_by, "sort_direction": sort_direction, "view": view, - "search": search, - "smeta_type": smeta_type, - "smeta_direction": smeta_direction, - "org_name": org_name, + "has_search": bool(search), + "search_len": len(search or ""), + "has_smeta_type": bool(smeta_type), + "has_smeta_direction": bool(smeta_direction), + "has_org_name": bool(org_name), "user": current_user.id, } logger.info(f"db_time: {extra}", extra=extra) @@ -244,10 +245,11 @@ async def get_summary2_facets( "sheet": sheet, "year": year, "org_ids": org_ids, - "search": search, - "smeta_type": smeta_type, - "smeta_direction": smeta_direction, - "org_name": org_name, + "has_search": bool(search), + "search_len": len(search or ""), + "has_smeta_type": bool(smeta_type), + "has_smeta_direction": bool(smeta_direction), + "has_org_name": bool(org_name), "user": current_user.id, } logger.info(f"db_time: {extra}", extra=extra) diff --git a/api/src/repository/summary_repository.py b/api/src/repository/summary_repository.py index 3d3ed7f..75d23b8 100644 --- a/api/src/repository/summary_repository.py +++ b/api/src/repository/summary_repository.py @@ -136,7 +136,7 @@ class SummaryRepository: }, ) ).all() - total = int(result[0][4]) if result else 0 + total = int(result[0].total_count) if result else 0 return [tuple(row[:4]) for row in result], total async def get_svod2_facets( diff --git a/api/tests/integration/test_svod2_paging_api.py b/api/tests/integration/test_svod2_paging_api.py index 795c1b5..8fb4bf3 100644 --- a/api/tests/integration/test_svod2_paging_api.py +++ b/api/tests/integration/test_svod2_paging_api.py @@ -18,6 +18,7 @@ SMETA_TYPE_VARIANT = SMETA_TYPE.lower() + " " SEARCHES = [ "аРЕНДА", "ё", "Ё", "ромаш", "лютик", "ромашка плюс", "1234.5", "1234.50", " аренда ", " ", "1", ",", "777.25", "", "я" * 5000, + "ооо лютик", "дс-2", "дс-1", "70", ] FILTERS = { @@ -140,6 +141,35 @@ def test_search_matches_screen_oracle(client, svod2_data, headers, sheet, search assert all(t == len(expected) for t in totals) +# Строка 910004 имеет contract_summary (ООО Лютик, ДС-2, 70), но НЕ имеет contract_detail: +# пустая секция contract_detail перекрывает counterparty/reference из contract_summary +# (семантика экрана), а неперекрытое поле total_amount остаётся доступным для поиска. +LINE_NO_DETAIL = 910004 + + +@pytest.mark.parametrize("sheet", SHEETS) +@pytest.mark.parametrize("search", ["ооо лютик", "дс-2"]) +def test_search_overlapped_contract_summary_field_not_found( + client, svod2_data, headers, sheet, search +): + full = legacy_full(client, sheet, headers) + expected = screen_rows(full, search) + assert LINE_NO_DETAIL not in _line_ids(expected) + rows, _ = _all_pages(client, sheet, headers, 3, view="compact", search=search) + assert LINE_NO_DETAIL not in _line_ids(rows) + + +@pytest.mark.parametrize("sheet", ["GO"]) +def test_search_non_overlapped_contract_summary_field_found( + client, svod2_data, headers, sheet +): + full = legacy_full(client, sheet, headers) + expected = screen_rows(full, "70") + assert LINE_NO_DETAIL in _line_ids(expected) + rows, _ = _all_pages(client, sheet, headers, 3, view="compact", search="70") + assert LINE_NO_DETAIL in _line_ids(rows) + + @pytest.mark.parametrize("sheet", SHEETS) @pytest.mark.parametrize("name", list(FILTERS)) def test_filters_match_screen_oracle(client, svod2_data, headers, sheet, name): diff --git a/api/tests/unit/test_summary2_log_privacy.py b/api/tests/unit/test_summary2_log_privacy.py new file mode 100644 index 0000000..f888aa9 --- /dev/null +++ b/api/tests/unit/test_summary2_log_privacy.py @@ -0,0 +1,60 @@ +"""Лог db_time не содержит значений поиска и фильтров (только признаки).""" +import logging +from types import SimpleNamespace + +import pytest +from fastapi import Response + +from src.api.v1 import summary as summary_api + +SECRET = "СЕКРЕТНЫЙ-ПОИСК-123" +FILTERS = {"smeta_type": "СЕКРЕТ-ТИП", "smeta_direction": "СЕКРЕТ-НАПР", "org_name": "СЕКРЕТ-ОРГ"} + + +class _FakeService: + def __init__(self, db): + pass + + async def get_svod2_page(self, **kwargs): + return [], 0 + + async def get_svod2_facets(self, **kwargs): + return {} + + +@pytest.fixture(autouse=True) +def _fake_service(monkeypatch): + monkeypatch.setattr(summary_api, "SummaryService", _FakeService) + monkeypatch.setattr(summary_api, "Svod2Facets", lambda **kw: kw, raising=False) + + +def _assert_no_values(caplog): + records = [r for r in caplog.records if getattr(r, "type", None) == "db_time"] + assert records + for r in records: + text = r.getMessage() + repr(r.__dict__) + for value in (SECRET, *FILTERS.values()): + assert value not in text + assert r.has_search is True + assert r.search_len == len(SECRET) + assert r.has_smeta_type and r.has_smeta_direction and r.has_org_name + + +@pytest.mark.asyncio +async def test_page_log_has_no_search_values(caplog): + caplog.set_level(logging.INFO) + await summary_api.get_summary2( + sheet="GO", year=2031, response=Response(), search=SECRET, db=None, + current_user=SimpleNamespace(id=1), view="compact", **FILTERS, + ) + _assert_no_values(caplog) + + +@pytest.mark.asyncio +async def test_facets_log_has_no_search_values(caplog): + caplog.set_level(logging.INFO) + await summary_api.get_summary2_facets( + sheet="GO", year=2031, response=Response(), search=SECRET, db=None, + current_user=SimpleNamespace(id=1), **FILTERS, + ) + _assert_no_values(caplog)