fix(reports): HEAD-поддержка на /report/download — qa поймал 405
All checks were successful
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 8s
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 2m35s
CI / backend-tests (pull_request) Successful in 14m43s

qa-смоук #2338: FastAPI НЕ добавляет HEAD к @router.get автоматически —
фронтовый HEAD-preflight DOCX получал 405 (allow: GET), abортился, и UI
молча не скачивал файл вовсе (curl GET при этом отдавал валидный DOCX
2.08 МБ). Явный @router.head (include_in_schema=False): FileResponse
при HEAD нативно отдаёт только заголовки. +2 теста (200 headers-only,
404 симметрично GET).
This commit is contained in:
bot-backend 2026-07-04 03:57:36 +05:00
parent 1d9e25ce07
commit 747d752b2d
2 changed files with 57 additions and 0 deletions

View file

@ -1699,6 +1699,10 @@ def parcel_report_status(
_REPORT_DOCX_MEDIA_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
# HEAD нужен фронтовому preflight'у (#2338): FastAPI НЕ добавляет HEAD к @router.get
# автоматически (прод отдавал 405 → UI не скачивал DOCX вовсе). FileResponse при
# HEAD нативно отдаёт только заголовки (Starlette send_header_only).
@router.head("/{cad_num}/report/download", include_in_schema=False)
@router.get(
"/{cad_num}/report/download",
summary="Скачать готовый полный отчёт участка — PDF или DOCX (#2259 PR-D/PR-F)",

View file

@ -422,3 +422,56 @@ def test_download_invalid_format_returns_422() -> None:
assert resp.status_code == 422, resp.text
finally:
app.dependency_overrides.clear()
def test_download_head_returns_headers_only(tmp_path: Path) -> None:
"""HEAD на download — 200 с заголовками БЕЗ тела (preflight фронта, #2338).
FastAPI не добавляет HEAD к @router.get автоматически прод отдавал 405,
HEAD-preflight абортился и UI молча не скачивал DOCX. Явный @router.head чинит.
"""
from app.core.db import get_db
pdf = tmp_path / "report.pdf"
pdf.write_bytes(b"%PDF-1.7 payload")
docx = tmp_path / "report.docx"
docx.write_bytes(b"PK\x03\x04 fake-docx")
report_row = _run(
510,
{
"pdf_path": str(pdf),
"docx_path": str(docx),
"analyze_run_id": 101,
"forecast_run_id": 202,
"generated_at": "2026-07-01T09:00:00+00:00",
},
)
rows = {REPORT_SCHEMA_VERSION: report_row}
db = MagicMock()
app.dependency_overrides[get_db] = _override_db(db)
try:
with patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router(rows)):
client = TestClient(app)
resp = client.head(f"/api/v1/parcels/{_CAD}/report/download?format=docx")
assert resp.status_code == 200, resp.text
assert resp.headers["content-type"] == (
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
)
assert resp.content == b""
finally:
app.dependency_overrides.clear()
def test_download_head_404_when_not_ready() -> None:
"""HEAD для участка без готового отчёта — 404 (симметрично GET)."""
from app.core.db import get_db
db = MagicMock()
app.dependency_overrides[get_db] = _override_db(db)
try:
with patch("app.api.v1.parcels.latest_run_for", side_effect=_lrf_router({})):
client = TestClient(app)
resp = client.head(f"/api/v1/parcels/{_CAD}/report/download")
assert resp.status_code == 404
finally:
app.dependency_overrides.clear()