Files
headquarter/apps/api/tests/test_users_api.py
T
Fusion 577b052c05 feat: implement user profile management and oauth/traefik integration
User Profile (US-004):
- Add authenticated profile endpoints (GET/PUT /users/me)
- Add avatar upload with file validation (PNG/JPEG, max 2MB)
- Create frontend profile page with edit form and avatar upload
- Update app shell to link to profile page

OAuth/Traefik Integration:
- Externalize all Authentik URLs to environment variables
- Add domain configuration (API_DOMAIN, WEB_DOMAIN, AUTHENTIK_DOMAIN)
- Create docker-compose.traefik.yml for reverse proxy deployment
- Update OAuth redirect/callback URLs to use configured domains
- Add VITE_APP_URL for frontend public URL configuration

Quality gates: pytest (50 passed), ruff, mypy, npm test (12 passed), typecheck, lint, build
2026-05-17 23:17:10 +02:00

226 lines
6.6 KiB
Python

import uuid
from datetime import UTC, datetime, timedelta
import asyncio
import io
from fastapi.testclient import TestClient
import pytest
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
from src.auth.jwt_service import mint_access_token
from src.config import Settings, build_database_url
from src.models import Base
from src.models.user import User
@pytest.fixture(autouse=True)
def configure_local_database(monkeypatch) -> None:
local_url = build_database_url(
user="headquarter",
password="headquarter",
host="localhost",
port=5432,
database="headquarter",
)
monkeypatch.setenv("DATABASE_URL", local_url)
def _prepare_users_test_db() -> None:
async def _run() -> None:
engine = create_async_engine(
build_database_url(
user="headquarter",
password="headquarter",
host="localhost",
port=5432,
database="headquarter",
)
)
async with engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
await connection.execute(text("TRUNCATE TABLE refresh_tokens, users RESTART IDENTITY CASCADE"))
await engine.dispose()
asyncio.run(_run())
def _load_app():
import importlib
import src.database as database_module
import src.api.users as users_module
import src.main as main_module
importlib.reload(database_module)
importlib.reload(users_module)
importlib.reload(main_module)
return main_module.app
def _insert_test_user(user_id: str) -> None:
async def _run() -> None:
engine = create_async_engine(
build_database_url(
user="headquarter",
password="headquarter",
host="localhost",
port=5432,
database="headquarter",
)
)
async with engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
from sqlalchemy.ext.asyncio import async_sessionmaker
session_factory = async_sessionmaker(engine, expire_on_commit=False)
async with session_factory() as session:
user = User(
id=uuid.UUID(user_id),
email="test@headquarter.local",
name="Test User",
authentik_id="test-user",
avatar_url=None,
)
await session.merge(user)
await session.commit()
await engine.dispose()
asyncio.run(_run())
def _create_auth_cookie(user_id: str) -> str:
settings = Settings()
return mint_access_token(
settings=settings,
subject=user_id,
email="test@headquarter.local",
name="Test User",
expires_at=datetime.now(UTC) + timedelta(minutes=15),
)
def test_get_profile_returns_401_without_cookie() -> None:
_prepare_users_test_db()
app = _load_app()
client = TestClient(app)
response = client.get("/users/me")
assert response.status_code == 401
def test_get_profile_returns_user_data() -> None:
_prepare_users_test_db()
user_id = "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb"
_insert_test_user(user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("access_token", _create_auth_cookie(user_id))
response = client.get("/users/me")
assert response.status_code == 200
data = response.json()
assert data["email"] == "test@headquarter.local"
assert data["name"] == "Test User"
assert data["avatar_url"] is None
def test_update_profile_changes_name_and_email() -> None:
_prepare_users_test_db()
user_id = "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb"
_insert_test_user(user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("access_token", _create_auth_cookie(user_id))
response = client.put("/users/me", json={"name": "Updated Name", "email": "updated@headquarter.local"})
assert response.status_code == 200
data = response.json()
assert data["name"] == "Updated Name"
assert data["email"] == "updated@headquarter.local"
def test_update_profile_rejects_empty_name() -> None:
_prepare_users_test_db()
user_id = "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb"
_insert_test_user(user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("access_token", _create_auth_cookie(user_id))
response = client.put("/users/me", json={"name": " "})
assert response.status_code == 400
def test_update_profile_rejects_invalid_email() -> None:
_prepare_users_test_db()
user_id = "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb"
_insert_test_user(user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("access_token", _create_auth_cookie(user_id))
response = client.put("/users/me", json={"email": "not-an-email"})
assert response.status_code == 400
def test_upload_avatar_updates_avatar_url() -> None:
_prepare_users_test_db()
user_id = "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb"
_insert_test_user(user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("access_token", _create_auth_cookie(user_id))
# Create a simple 1x1 PNG
png_data = b"\x89PNG\r\n\x1a\n" + b"\x00" * 100
response = client.post(
"/users/me/avatar",
files={"file": ("test.png", io.BytesIO(png_data), "image/png")},
)
assert response.status_code == 200
data = response.json()
assert data["avatar_url"] is not None
assert data["avatar_url"].startswith("/uploads/avatars/")
def test_upload_avatar_rejects_invalid_file_type() -> None:
_prepare_users_test_db()
user_id = "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb"
_insert_test_user(user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("access_token", _create_auth_cookie(user_id))
response = client.post(
"/users/me/avatar",
files={"file": ("test.txt", io.BytesIO(b"not an image"), "text/plain")},
)
assert response.status_code == 400
def test_upload_avatar_rejects_oversized_file() -> None:
_prepare_users_test_db()
user_id = "7f4b7ad8-c4ce-4d1b-8c83-7ce0f4f66dfb"
_insert_test_user(user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("access_token", _create_auth_cookie(user_id))
large_png = b"\x89PNG\r\n\x1a\n" + b"\x00" * (3 * 1024 * 1024) # 3MB
response = client.post(
"/users/me/avatar",
files={"file": ("large.png", io.BytesIO(large_png), "image/png")},
)
assert response.status_code == 400