fix(#190): экранировать формулы в CSV-выгрузке качества (T15.1)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-07 02:46:25 +04:00
parent 1d99ea265e
commit 3589f53c3c
2 changed files with 18 additions and 1 deletions

View File

@ -354,6 +354,9 @@ class QualityMetricsService:
return buffer.getvalue().encode("utf-8-sig"), "text/csv; charset=utf-8", "csv"
_FORMULA_PREFIXES = ("=", "+", "-", "@", "\t", "\r")
def _cell(value: Any) -> Any:
"""Значение ячейки CSV: перечисления — их значение, даты — ISO, булевы — true/false."""
if isinstance(value, bool):
@ -362,4 +365,6 @@ def _cell(value: Any) -> Any:
return value.value
if isinstance(value, datetime | date):
return value.isoformat()
if isinstance(value, str) and value.startswith(_FORMULA_PREFIXES):
return "'" + value # защита от CSV-инъекции формул в Excel
return value

View File

@ -30,7 +30,7 @@ from src.models.enums import (
)
from src.repositories.quality_facts import insert_failure_fact, insert_intake_fact, insert_outcome_fact
from src.repositories.users import UserWithAccess
from src.services.quality_metrics_service import EXPORT_COLUMNS
from src.services.quality_metrics_service import EXPORT_COLUMNS, _cell
from tests.conftest import AuthClientFactory
HTTP_OK, HTTP_FORBIDDEN, HTTP_UNPROCESSABLE = 200, 403, 422
@ -322,3 +322,15 @@ async def test_quality_metrics_export(auth_client: AuthClientFactory, seeded: As
assert rows[0]["is_scan"] == "true"
as_json = await client.get(f"{BASE}/export", params={"format": "json", "date_from": "2026-11-01"})
assert len(as_json.json()) == 2 # noqa: PLR2004
@pytest.mark.parametrize("prefix", ["=", "+", "-", "@", "\t", "\r"])
def test_csv_cell_escapes_formula_strings(prefix: str) -> None:
assert _cell(f"{prefix}SUM(A1)") == f"'{prefix}SUM(A1)"
def test_csv_cell_keeps_non_strings_and_plain_strings() -> None:
assert _cell(-5) == -5 # noqa: PLR2004
assert _cell("ООО Альфа") == "ООО Альфа"
assert _cell(datetime.date(2026, 11, 1)) == "2026-11-01"
assert _cell(True) == "true"