refactor: align backend layout and supabase infra

Consolidate backend modules/tests under the backend package while syncing Supabase compose/env config and related plans.
This commit is contained in:
qzl
2026-02-05 15:13:06 +08:00
parent 3cfcb11240
commit ad06fe7de4
111 changed files with 5540 additions and 1362 deletions
@@ -0,0 +1,49 @@
from __future__ import annotations
import socket
import pytest
from core.config.settings import Settings
from services.base.qdrant import QdrantService
from services.base.redis import RedisService
def _can_connect(host: str, port: int) -> bool:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.settimeout(0.2)
return sock.connect_ex((host, port)) == 0
@pytest.mark.asyncio
async def test_redis_service_health_check_integration() -> None:
host = "127.0.0.1"
port = 6379
if not _can_connect(host, port):
pytest.skip("Redis is not running on localhost:6379")
config = Settings()
settings = config.redis.model_copy(update={"host": host, "port": port})
service = RedisService(settings=settings)
assert await service.initialize() is True
health = await service.health_check()
assert health["status"] == "healthy"
assert await service.close() is True
@pytest.mark.asyncio
async def test_qdrant_service_health_check_integration() -> None:
host = "127.0.0.1"
port = 6333
if not _can_connect(host, port):
pytest.skip("Qdrant is not running on localhost:6333")
config = Settings()
settings = config.qdrant.model_copy(update={"host": host, "port": port})
service = QdrantService(settings=settings)
assert await service.initialize() is True
health = await service.health_check()
assert health["status"] == "healthy"
assert await service.close() is True
@@ -0,0 +1,178 @@
from __future__ import annotations
from typing import Callable
from fastapi import HTTPException
from fastapi.testclient import TestClient
from app import app
from v1.auth.dependencies import get_auth_service
from v1.auth.models import (
AuthTokenResponse,
AuthUser,
LoginRequest,
RefreshRequest,
SignupRequest,
)
from v1.auth.service import AuthService
class FakeAuthService(AuthService):
def __init__(self, token_response: AuthTokenResponse) -> None:
self._token_response = token_response
async def signup(self, request: SignupRequest) -> AuthTokenResponse:
return self._token_response
async def login(self, request: LoginRequest) -> AuthTokenResponse:
raise HTTPException(status_code=401, detail="Invalid credentials")
async def refresh(self, request: RefreshRequest) -> AuthTokenResponse:
raise HTTPException(status_code=401, detail="Invalid refresh token")
async def logout(self, refresh_token: str | None) -> None:
return None
def _override_auth_service(service: AuthService) -> Callable[[], AuthService]:
def _get_service() -> AuthService:
return service
return _get_service
def test_signup_returns_token_response() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
token_type="bearer",
user=user,
)
app.dependency_overrides[get_auth_service] = _override_auth_service(
FakeAuthService(token_response)
)
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/signup",
json={"email": "user@example.com", "password": "secret123"},
)
assert response.status_code == 200
body = response.json()
assert body["access_token"] == "access"
assert body["refresh_token"] == "refresh"
assert body["user"]["email"] == "user@example.com"
finally:
app.dependency_overrides = {}
def test_login_invalid_returns_problem_details() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
token_type="bearer",
user=user,
)
app.dependency_overrides[get_auth_service] = _override_auth_service(
FakeAuthService(token_response)
)
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/login",
json={"email": "user@example.com", "password": "wrongpw"},
)
assert response.status_code == 401
assert response.headers["content-type"].startswith("application/problem+json")
body = response.json()
assert body["title"] == "Unauthorized"
assert body["status"] == 401
assert body["detail"] == "Invalid credentials"
finally:
app.dependency_overrides = {}
def test_refresh_invalid_returns_problem_details() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
token_type="bearer",
user=user,
)
app.dependency_overrides[get_auth_service] = _override_auth_service(
FakeAuthService(token_response)
)
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/refresh",
json={"refresh_token": "invalid"},
)
assert response.status_code == 401
assert response.headers["content-type"].startswith("application/problem+json")
body = response.json()
assert body["title"] == "Unauthorized"
assert body["status"] == 401
assert body["detail"] == "Invalid refresh token"
finally:
app.dependency_overrides = {}
def test_logout_returns_no_content() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
token_type="bearer",
user=user,
)
app.dependency_overrides[get_auth_service] = _override_auth_service(
FakeAuthService(token_response)
)
client = TestClient(app)
try:
response = client.post(
"/api/v1/auth/logout",
json={"refresh_token": "refresh"},
)
assert response.status_code == 204
assert response.content == b""
finally:
app.dependency_overrides = {}
def test_signup_validation_error_returns_problem_details() -> None:
user = AuthUser(id="user-1", email="user@example.com")
token_response = AuthTokenResponse(
access_token="access",
refresh_token="refresh",
expires_in=3600,
token_type="bearer",
user=user,
)
app.dependency_overrides[get_auth_service] = _override_auth_service(
FakeAuthService(token_response)
)
client = TestClient(app)
try:
response = client.post("/api/v1/auth/signup", json={})
assert response.status_code == 422
assert response.headers["content-type"].startswith("application/problem+json")
body = response.json()
assert body["title"] == "Unprocessable Content"
assert body["status"] == 422
assert body["detail"] == "Invalid request"
finally:
app.dependency_overrides = {}
@@ -0,0 +1,126 @@
from __future__ import annotations
import json
import logging
from pathlib import Path
from fastapi import FastAPI
from fastapi.testclient import TestClient
from core.config.settings import Settings
from core.logging.config import configure_logging
from core.logging.logger import get_logger
from core.logging.middleware import (
RequestContextMiddleware,
register_exception_handlers,
)
def _read_json_lines(path: Path) -> list[dict[str, object]]:
return [json.loads(line) for line in path.read_text().splitlines() if line.strip()]
def _configure_test_logging(tmp_path: Path) -> None:
settings = Settings()
runtime = settings.runtime.model_copy(
update={
"log_dir": str(tmp_path),
"log_error_dir": str(tmp_path / "errors"),
"log_rotation": "size",
"log_rotation_max_bytes": 2048,
}
)
test_settings = settings.model_copy(update={"runtime": runtime})
configure_logging(test_settings)
def test_middleware_binds_request_context(tmp_path: Path) -> None:
_configure_test_logging(tmp_path)
app = FastAPI()
app.add_middleware(RequestContextMiddleware) # type: ignore[arg-type]
@app.get("/ok")
async def ok() -> dict[str, str]:
logger = get_logger("tests.ok")
logger.info("request accepted", context_key="context_value")
return {"status": "ok"}
client = TestClient(app)
response = client.get("/ok", headers={"X-Request-ID": "req-1234"})
assert response.status_code == 200
assert response.headers["X-Request-ID"] == "req-1234"
log_entries = _read_json_lines(Path(tmp_path) / "app.log")
entry = next(
item for item in log_entries if item.get("message") == "request accepted"
)
assert entry["message"] == "request accepted"
assert entry["request_id"] == "req-1234"
assert entry["method"] == "GET"
assert entry["path"] == "/ok"
assert entry["context_key"] == "context_value"
logging.shutdown()
def test_exception_handler_logs_stack_and_sends_500(tmp_path: Path) -> None:
_configure_test_logging(tmp_path)
app = FastAPI()
app.add_middleware(RequestContextMiddleware)
register_exception_handlers(app)
@app.get("/boom")
async def boom() -> dict[str, str]:
raise RuntimeError("boom")
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/boom", headers={"X-Request-ID": "req-5000"})
assert response.status_code == 500
assert response.json()["detail"] == "Internal Server Error"
error_entries = _read_json_lines(Path(tmp_path) / "errors" / "error.log")
assert error_entries
entry = error_entries[-1]
assert entry["level"] == "error"
assert entry["request_id"] == "req-5000"
exception = str(entry["exception"])
assert "Traceback" in exception
assert "test_fastapi_logging_integration" in exception
logging.shutdown()
def test_invalid_request_id_is_replaced_and_used_in_error_context(
tmp_path: Path,
) -> None:
_configure_test_logging(tmp_path)
app = FastAPI()
app.add_middleware(RequestContextMiddleware)
register_exception_handlers(app)
@app.get("/boom")
async def boom() -> dict[str, str]:
raise RuntimeError("boom")
client = TestClient(app, raise_server_exceptions=False)
response = client.get("/boom", headers={"X-Request-ID": "bad"})
assert response.status_code == 500
response_request_id = response.headers["X-Request-ID"]
assert response_request_id != "bad"
error_entries = _read_json_lines(Path(tmp_path) / "errors" / "error.log")
assert error_entries
entry = error_entries[-1]
assert entry["request_id"] == response_request_id
exception = str(entry["exception"])
assert "Traceback" in exception
logging.shutdown()
@@ -0,0 +1,39 @@
from __future__ import annotations
from fastapi.testclient import TestClient
from app import app
def test_app_health_returns_envelope() -> None:
client = TestClient(app)
response = client.get("/health")
assert response.status_code == 200
body = response.json()
assert body["status"] == "ok"
def test_mobile_router_health_returns_envelope() -> None:
client = TestClient(app)
response = client.get("/api/v1/health")
assert response.status_code == 200
body = response.json()
assert body["status"] == "ok"
def test_not_found_returns_error_envelope() -> None:
client = TestClient(app)
response = client.get("/missing-route")
assert response.status_code == 404
assert response.headers["content-type"].startswith("application/problem+json")
body = response.json()
assert body["type"] == "about:blank"
assert body["title"] == "Not Found"
assert body["status"] == 404
assert body["detail"] == "Not Found"
@@ -0,0 +1,188 @@
from __future__ import annotations
from typing import Callable
from uuid import UUID
from fastapi import HTTPException
from fastapi.testclient import TestClient
from app import app
from core.auth.models import CurrentUser
from v1.profile.dependencies import get_current_user, get_profile_service
from v1.profile.schemas import ProfileResponse, ProfileUpdateRequest
from v1.profile.service import ProfileService
class FakeProfileService:
"""Fake service for integration testing."""
def __init__(self, profile: ProfileResponse) -> None:
self._profile = profile
async def get_me(self) -> ProfileResponse:
if self._profile.id is None:
raise HTTPException(status_code=404, detail="Profile not found")
return self._profile
async def update_me(self, update: ProfileUpdateRequest) -> ProfileResponse:
if self._profile.id is None:
raise HTTPException(status_code=404, detail="Profile not found")
return ProfileResponse(
id=self._profile.id,
username=self._profile.username,
display_name=(
update.display_name
if update.display_name is not None
else self._profile.display_name
),
avatar_url=(
update.avatar_url
if update.avatar_url is not None
else self._profile.avatar_url
),
bio=update.bio if update.bio is not None else self._profile.bio,
)
async def get_by_username(self, username: str) -> ProfileResponse:
if username != self._profile.username:
raise HTTPException(status_code=404, detail="Profile not found")
return self._profile
def _override_profile_service(
service: FakeProfileService,
) -> Callable[[], ProfileService]:
def _get_service() -> ProfileService:
return service # type: ignore[return-value]
return _get_service
def _override_current_user(user_id: UUID) -> Callable[[], CurrentUser]:
def _get_user() -> CurrentUser:
return CurrentUser(id=user_id)
return _get_user
def test_get_me_returns_profile() -> None:
user_id = UUID("00000000-0000-0000-0000-000000000001")
profile = ProfileResponse(
id=str(user_id),
username="demo",
display_name="Demo User",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_profile_service] = _override_profile_service(
FakeProfileService(profile)
)
app.dependency_overrides[get_current_user] = _override_current_user(user_id)
client = TestClient(app)
try:
response = client.get("/api/v1/profile/me")
assert response.status_code == 200
body = response.json()
assert body["username"] == "demo"
finally:
app.dependency_overrides = {}
def test_patch_me_updates_profile() -> None:
user_id = UUID("00000000-0000-0000-0000-000000000001")
profile = ProfileResponse(
id=str(user_id),
username="demo",
display_name="Demo User",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_profile_service] = _override_profile_service(
FakeProfileService(profile)
)
app.dependency_overrides[get_current_user] = _override_current_user(user_id)
client = TestClient(app)
try:
response = client.patch(
"/api/v1/profile/me",
json={"display_name": "Updated"},
)
assert response.status_code == 200
body = response.json()
assert body["display_name"] == "Updated"
finally:
app.dependency_overrides = {}
def test_get_profile_by_username() -> None:
profile = ProfileResponse(
id="00000000-0000-0000-0000-000000000001",
username="demo",
display_name="Demo User",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_profile_service] = _override_profile_service(
FakeProfileService(profile)
)
client = TestClient(app)
try:
response = client.get("/api/v1/profile/demo")
assert response.status_code == 200
body = response.json()
assert body["username"] == "demo"
finally:
app.dependency_overrides = {}
def test_profile_not_found_returns_problem_details() -> None:
profile = ProfileResponse(
id="00000000-0000-0000-0000-000000000001",
username="demo",
display_name="Demo User",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_profile_service] = _override_profile_service(
FakeProfileService(profile)
)
client = TestClient(app)
try:
response = client.get("/api/v1/profile/unknown")
assert response.status_code == 404
assert response.headers["content-type"].startswith("application/problem+json")
body = response.json()
assert body["title"] == "Not Found"
assert body["status"] == 404
finally:
app.dependency_overrides = {}
def test_patch_me_validation_error_returns_problem_details() -> None:
user_id = UUID("00000000-0000-0000-0000-000000000001")
profile = ProfileResponse(
id=str(user_id),
username="demo",
display_name="Demo User",
avatar_url=None,
bio=None,
)
app.dependency_overrides[get_profile_service] = _override_profile_service(
FakeProfileService(profile)
)
app.dependency_overrides[get_current_user] = _override_current_user(user_id)
client = TestClient(app)
try:
response = client.patch("/api/v1/profile/me", json={})
assert response.status_code == 422
assert response.headers["content-type"].startswith("application/problem+json")
body = response.json()
assert body["title"] == "Unprocessable Content"
assert body["status"] == 422
finally:
app.dependency_overrides = {}