All checks were successful
CI / changes (pull_request) Successful in 12s
CI Trade-In / changes (pull_request) Successful in 12s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 1m12s
Блокер номер один перед открытием эндпоинта оценки наружу: анонимный запрос означал БЕЗЛИМИТ. В сервисе квот отсутствие имени пользователя трактовалось как unlimited во всех функциях, с комментарием «dev без Caddy, fail-open». Единственной защитой был общий лимит 300 запросов в минуту на IP — это анти-флуд для дешёвых запросов, а не бизнес-лимит для пайплайна на десятки секунд. 1. Fail-open больше не по умолчанию. Вызывающая сторона передаёт пустую личность только при явно включённом флаге разработки (по умолчанию выкл). 2. Анонимная личность — подписанная кука с HMAC-SHA256, отдельным каналом от X-Authenticated-User. Тот заголовок ставит Caddy и валидирует внутренним секретом; смешивать схемы нельзя, это сломало бы модель безопасности. Ключ подписи из окружения; если не задан — эфемерный на процесс, с предупреждением в лог. 3. Анонимная квота на паре «сессия + IP», переиспользует существующую таблицу и тот же атомарный инкремент под WHERE used < lim (защита от гонки #747). Честно закомментировано: смена IP или чистка куки обходит лимит — задача поднять стоимость злоупотребления, а не сделать его невозможным. 4. Отдельный жёсткий лимит частоты на оценку, проверяется ДО квоты. Переиспользован готовый SlidingWindowLimiter. Redis намеренно не задействован: прод работает одним воркером, состояние теряется только при рестарте, а основная защита — месячная квота в Postgres. Компромисс задокументирован. 5. Потолок времени ответа. Вызов Avito IMV шёл БЕЗ бюджета, в отличие от всех соседних — единственный источник неограниченного времени. Обёрнут. Суммарный худший случай: было ~186 с (36 с ограниченных плюс IMV без границы ~150 с), стало 56 с. Тесты: 2754 passed. Два существующих теста обновлены под изменившееся поведение fail-open — это ожидаемое изменение, не регрессия.
44 lines
1.9 KiB
Python
44 lines
1.9 KiB
Python
"""Repo-wide test config for tradein-mvp/backend.
|
|
|
|
Registers custom pytest markers so they don't emit PytestUnknownMarkWarning
|
|
when used (`--strict-markers` is not enabled in pyproject.toml, so an
|
|
unregistered marker would only warn, not fail — this just keeps output clean
|
|
and documents intent in one place). Also resets cross-test-file shared
|
|
rate-limiter state (see fixture docstring below).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
|
|
def pytest_configure(config) -> None:
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"pdf_render: real (non-mocked) WeasyPrint render — needs native "
|
|
"Pango/cairo/GObject libs, self-skips where unavailable (see "
|
|
"tests/test_pdf_real_render.py docstring for how to run it for real).",
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _reset_estimate_rate_limiter() -> None:
|
|
"""`app.api.v1.trade_in._estimate_limiter` (#b2c-antiabuse-2) is a module-level
|
|
`SlidingWindowLimiter` singleton that accumulates hits across ALL tests
|
|
hitting POST /estimate within one pytest process (a dozen+ test files build
|
|
their own FastAPI app around `trade_in_module.router` — see grep for
|
|
`trade_in_module.router` under tests/). Without a reset, unrelated test
|
|
files could trip the 429 rate-limit purely from cross-test state leakage
|
|
(same class of issue `test_support.py::_fresh_rate_limiter` solves locally
|
|
for `_send_limiter` — this one needs to be global since so many files touch
|
|
the trade_in router). Lazy import: keeps conftest.py import-light and avoids
|
|
forcing DATABASE_URL to be set before any test module has had a chance to
|
|
default it.
|
|
"""
|
|
from app.api.v1 import trade_in as trade_in_module
|
|
from app.core.config import settings
|
|
from app.core.ratelimit import SlidingWindowLimiter
|
|
|
|
trade_in_module._estimate_limiter = SlidingWindowLimiter(
|
|
limit=settings.estimate_rate_limit, window_s=settings.estimate_rate_limit_window_s
|
|
)
|