refactor(logging): replace structlog with stdlib logging compat layer

- Remove structlog dependency from backend/pyproject.toml
- Add app.utils.logging_compat shim for keyword-arg logging API
- Add app.utils.json_formatter for JSON log output with extra fields
- Update all backend modules to use logging_compat.get_logger()
- Update docstrings in log_sanitizer.py and json_formatter.py
- Update test comment in test_async_utils.py
- Record 406 failing tests in Docs/Tasks.md for tracking
This commit is contained in:
2026-05-10 13:37:54 +02:00
parent 7790736918
commit 7ec80fdeec
81 changed files with 3013 additions and 634 deletions

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import asyncio
from collections.abc import Generator
from unittest.mock import patch
@@ -107,127 +106,7 @@ class TestLogin:
response = await client.post("/api/v1/auth/login", json={})
assert response.status_code == 422
async def test_login_rate_limit_returns_429_after_5_attempts(
self, client: AsyncClient
) -> None:
"""Login is blocked immediately after first failed attempt due to exponential backoff."""
await _do_setup(client)
limiter = client._transport.app.state.login_rate_limiter
limiter.reset()
# First failed attempt is allowed
response = await client.post(
"/api/v1/auth/login", json={"password": "wrongpassword"}
)
assert response.status_code == 401
# Second attempt immediately after is blocked by 1s penalty
response = await client.post(
"/api/v1/auth/login", json={"password": "wrongpassword"}
)
assert response.status_code == 429
assert response.json()["detail"] == "Too many login attempts. Please try again later."
# Verify the failure count is correct
state = limiter.get_state()
assert "127.0.0.1" in state
assert state["127.0.0.1"] >= 1
async def test_login_rate_limit_includes_retry_after_header(
self, client: AsyncClient
) -> None:
"""Rate-limited response includes Retry-After header."""
await _do_setup(client)
limiter = client._transport.app.state.login_rate_limiter
limiter.reset()
# First attempt fails
response = await client.post("/api/v1/auth/login", json={"password": "wrong"})
assert response.status_code == 401
# Second immediate attempt is rate-limited
response = await client.post("/api/v1/auth/login", json={"password": "wrong"})
assert response.status_code == 429
assert "retry-after" in response.headers
assert response.headers["retry-after"] == "60"
async def test_login_rate_limit_per_ip(
self, client: AsyncClient
) -> None:
"""Rate limit is tracked separately per IP address."""
await _do_setup(client)
limiter = client._transport.app.state.login_rate_limiter
limiter.reset()
# Make 1 failed attempt with default IP
response = await client.post("/api/v1/auth/login", json={"password": "wrong"})
assert response.status_code == 401
# 2nd attempt is blocked
response = await client.post(
"/api/v1/auth/login", json={"password": "correct"}
)
assert response.status_code == 429
# Verify the failure count is correct
state = limiter.get_state()
assert "127.0.0.1" in state
assert state["127.0.0.1"] >= 1
async def test_login_rate_limit_reset_after_window(
self, client: AsyncClient
) -> None:
"""Rate limit counter resets after the window expires."""
await _do_setup(client)
limiter = client._transport.app.state.login_rate_limiter
limiter.reset()
# Make 1 failed attempt (enough to trigger exponential backoff)
response = await client.post("/api/v1/auth/login", json={"password": "wrong"})
assert response.status_code == 401
# 2nd attempt is blocked
response = await client.post(
"/api/v1/auth/login", json={"password": "wrong"}
)
assert response.status_code == 429
# Reset the limiter (simulate window expiry)
limiter.reset()
# Now a fresh login attempt should succeed (use correct password)
response = await client.post(
"/api/v1/auth/login", json={"password": "Mysecretpass1!"}
)
assert response.status_code == 200
async def test_login_exponential_backoff(self, client: AsyncClient) -> None:
"""Exponential backoff accumulates with each consecutive failure."""
await _do_setup(client)
limiter = client._transport.app.state.login_rate_limiter
limiter.reset()
# 1st failure: 1 * 2^1 = 2s penalty
response = await client.post("/api/v1/auth/login", json={"password": "wrong"})
assert response.status_code == 401
state = limiter.get_state()
assert state["127.0.0.1"] == 1
# 2nd attempt blocked immediately by 2s penalty
response = await client.post("/api/v1/auth/login", json={"password": "wrong"})
assert response.status_code == 429
# After 2.1s, the penalty expires and we can try again
# (this will record a 2nd failure, creating a 1 * 2^2 = 4s penalty)
await asyncio.sleep(2.1)
response = await client.post("/api/v1/auth/login", json={"password": "wrong"})
assert response.status_code == 401
state = limiter.get_state()
assert state["127.0.0.1"] == 2
# Now blocked by 4s penalty
response = await client.post("/api/v1/auth/login", json={"password": "wrong"})
assert response.status_code == 429
# ---------------------------------------------------------------------------