gendesign/tradein-mvp/backend/tests/test_request_audit.py
lekss361 32037af6de
All checks were successful
Deploy Trade-In / changes (push) Successful in 11s
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / test (push) Successful in 48s
Deploy Trade-In / build-backend (push) Successful in 1m0s
Deploy Trade-In / deploy (push) Successful in 51s
feat(tradein/audit): request-audit middleware + estimate_request logging → user_events (#2519)
RequestAuditMiddleware logs api_request + deduped login per authenticated /api/* request; estimate() logs estimate_request with address. Fire-and-forget, never blocks/breaks the request. Writes to user_events (Feature 2/3 capture).
2026-07-13 20:09:22 +00:00

116 lines
4.1 KiB
Python

"""Tests for RequestAuditMiddleware (app/core/request_audit.py).
Тестируем middleware в изоляции на минимальном FastAPI-приложении (как
tests/test_ratelimit.py) — не тянем тяжёлый app.main. schedule_event
и should_log_login мокируются, реальная запись в user_events не требуется
(она уже покрыта tests/test_user_events.py).
"""
from __future__ import annotations
import os
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
from unittest.mock import patch
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.core.request_audit import RequestAuditMiddleware
@pytest.fixture
def client() -> TestClient:
app = FastAPI()
app.add_middleware(RequestAuditMiddleware)
@app.get("/api/v1/ping")
def ping() -> dict[str, bool]:
return {"ok": True}
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
return TestClient(app)
def test_authenticated_api_request_schedules_api_request_event(client: TestClient) -> None:
with (
patch("app.core.request_audit.schedule_event") as mock_schedule,
patch("app.core.request_audit.should_log_login", return_value=False),
):
resp = client.get("/api/v1/ping", headers={"X-Authenticated-User": "alice"})
assert resp.status_code == 200
mock_schedule.assert_called_once()
kwargs = mock_schedule.call_args.kwargs
assert kwargs["event_type"] == "api_request"
assert kwargs["username"] == "alice"
assert kwargs["path"] == "/api/v1/ping"
assert kwargs["method"] == "GET"
def test_login_event_scheduled_when_should_log_login_true(client: TestClient) -> None:
with (
patch("app.core.request_audit.schedule_event") as mock_schedule,
patch("app.core.request_audit.should_log_login", return_value=True),
):
client.get("/api/v1/ping", headers={"X-Authenticated-User": "alice"})
event_types = [c.kwargs["event_type"] for c in mock_schedule.call_args_list]
assert event_types == ["api_request", "login"]
def test_login_event_not_scheduled_when_should_log_login_false(client: TestClient) -> None:
with (
patch("app.core.request_audit.schedule_event") as mock_schedule,
patch("app.core.request_audit.should_log_login", return_value=False),
):
client.get("/api/v1/ping", headers={"X-Authenticated-User": "alice"})
event_types = [c.kwargs["event_type"] for c in mock_schedule.call_args_list]
assert event_types == ["api_request"]
def test_no_username_header_skips_audit(client: TestClient) -> None:
with patch("app.core.request_audit.schedule_event") as mock_schedule:
resp = client.get("/api/v1/ping")
assert resp.status_code == 200
mock_schedule.assert_not_called()
def test_public_path_skips_audit_even_with_username(client: TestClient) -> None:
with patch("app.core.request_audit.schedule_event") as mock_schedule:
resp = client.get("/health", headers={"X-Authenticated-User": "alice"})
assert resp.status_code == 200
mock_schedule.assert_not_called()
def test_non_api_path_skips_audit(client: TestClient) -> None:
"""Path not under /api/ (and not one of the FastAPI-generated docs paths) is skipped."""
app = FastAPI()
app.add_middleware(RequestAuditMiddleware)
@app.get("/other")
def other() -> dict[str, bool]:
return {"ok": True}
with patch("app.core.request_audit.schedule_event") as mock_schedule:
resp = TestClient(app).get("/other", headers={"X-Authenticated-User": "alice"})
assert resp.status_code == 200
mock_schedule.assert_not_called()
def test_audit_failure_does_not_break_response(client: TestClient) -> None:
"""A raising schedule_event must never turn a 200 into a 500 (dispatch swallows it)."""
with patch("app.core.request_audit.schedule_event", side_effect=RuntimeError("boom")):
resp = client.get("/api/v1/ping", headers={"X-Authenticated-User": "alice"})
assert resp.status_code == 200
assert resp.json() == {"ok": True}