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
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).
116 lines
4.1 KiB
Python
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}
|