fix(admin): secure routes, add real IP detection, SMTP header validation, and fix Next.js layout hydration mismatch
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m5s

This commit is contained in:
2026-06-01 23:16:03 +02:00
parent 6d27dc4cda
commit 6da8a85b1d
10 changed files with 1165 additions and 96 deletions

View File

@@ -1,16 +1,21 @@
"""
Test configuration and fixtures
"""
from typing import AsyncGenerator
import pytest
import pytest_asyncio
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from database.connection import sync_engine
from database.models import Base
# In-memory SQLite: fully isolated, no disk state between test sessions
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
@pytest.fixture(scope="session", autouse=True)
def initialize_test_database():
Base.metadata.create_all(bind=sync_engine)
yield
@pytest_asyncio.fixture
async def async_engine():
from database.models import Base

View File

@@ -123,7 +123,7 @@ def test_admin_logs_returns_200_and_shape(client_with_admin, admin_token):
def test_admin_logs_no_original_filename_in_response(client_with_admin, admin_token):
"""NFR11/NFR16: response must never contain original_filename or document content."""
row = _make_mock_translation(original_filename="sensitive.docx")
with patch("routes.admin_routes.get_sync_session") as mock_get_session:
with patch("database.connection.get_sync_session") as mock_get_session:
session_mock = MagicMock()
mock_get_session.return_value.__enter__.return_value = session_mock
mock_get_session.return_value.__exit__.return_value = None

View File

@@ -200,7 +200,7 @@ def app_client_for_quota(tmp_path, monkeypatch, admin_password):
monkeypatch.setattr(auth_svc, "USERS_FILE", tmp_path / "users.json")
monkeypatch.setattr(auth_svc, "USE_DATABASE", False)
monkeypatch.setattr(auth_svc, "DATABASE_AVAILABLE", False)
monkeypatch.setattr(tier_quota_mod, "_async_redis", None)
monkeypatch.setattr(tier_quota_mod, "_get_async_redis", lambda: None)
monkeypatch.setenv("REDIS_URL", "")
_memory_usage.clear()
@@ -267,8 +267,11 @@ def test_after_upgrade_to_pro_user_can_translate_beyond_five(
Path(output_path).write_bytes(b"dummy")
with patch(
"translators.excel_translator.excel_translator.translate_file",
"routes.translate_routes.ExcelTranslator.translate_file",
side_effect=_fake_translate,
), patch(
"routes.translate_routes.ExcelTranslator.get_translation_stats",
return_value={"attempted": 1, "changed": 1},
):
for _ in range(6):
with open(minimal_xlsx, "rb") as f:
@@ -333,8 +336,11 @@ def test_after_downgrade_to_free_quota_five_applies(
Path(output_path).write_bytes(b"dummy")
with patch(
"translators.excel_translator.excel_translator.translate_file",
"routes.translate_routes.ExcelTranslator.translate_file",
side_effect=_fake_translate,
), patch(
"routes.translate_routes.ExcelTranslator.get_translation_stats",
return_value={"attempted": 1, "changed": 1},
):
for _ in range(5):
with open(minimal_xlsx, "rb") as f:
@@ -350,6 +356,8 @@ def test_after_downgrade_to_free_quota_five_applies(
data={"target_lang": "fr", "provider": "google"},
headers={"Authorization": f"Bearer {access_token}"},
)
import time
time.sleep(0.5)
client.patch(
f"{ADMIN_USERS_PATCH}/{user_id}",
json={"plan": "free"},

View File

@@ -19,11 +19,15 @@ from middleware import tier_quota as tier_quota_mod
from middleware.tier_quota import (
TierQuotaService,
QuotaResult,
FREE_TIER_DAILY_LIMIT,
FREE_TIER_MONTHLY_LIMIT as FREE_TIER_DAILY_LIMIT,
_memory_usage,
_seconds_until_midnight_utc,
)
def _seconds_until_midnight_utc():
from middleware.tier_quota import _seconds_until_next_month
return _seconds_until_next_month()
# Force in-memory backend and reset state so tests are isolated
@pytest.fixture(autouse=True)