from __future__ import annotations from datetime import datetime, timezone from fastapi.testclient import TestClient from app.domain.models import User from app.main import app from app.services.user_service import UserService, get_user_service class FakeUserRepo: def __init__(self) -> None: self.users: dict[str, User] = {} self.next_id = 1 def get_user(self, user_id: str) -> User: user = self.users.get(user_id) if user is None: raise ValueError("사용자를 찾을 수 없습니다") return user def list_users(self) -> list[User]: return sorted(self.users.values(), key=lambda user: user.username) def create_user(self, username: str, *, email: str | None = None) -> User: if any(user.username == username for user in self.users.values()): raise ValueError(f"이미 존재하는 username입니다: {username}") if email is not None and any(user.email == email for user in self.users.values()): raise ValueError(f"이미 존재하는 email입니다: {email}") now = datetime.now(timezone.utc) user = User( id=f"user-{self.next_id}", username=username, email=email, created_at=now, updated_at=now, ) self.next_id += 1 self.users[user.id] = user return user def update_user(self, user_id: str, *, username: str | None = None, email: str | None = None) -> User: user = self.get_user(user_id) if username is not None and any(item.id != user_id and item.username == username for item in self.users.values()): raise ValueError(f"이미 존재하는 username입니다: {username}") if email is not None and any(item.id != user_id and item.email == email for item in self.users.values()): raise ValueError(f"이미 존재하는 email입니다: {email}") if username is not None: user.username = username if email is not None: user.email = email user.updated_at = datetime.now(timezone.utc) return user def delete_user(self, user_id: str) -> None: if user_id not in self.users: raise ValueError("사용자를 찾을 수 없습니다") del self.users[user_id] def _client_with_repo(monkeypatch, repo: FakeUserRepo) -> TestClient: monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "false") monkeypatch.setenv("PORTFOLIOHUB_ADMIN_TOKENS", "admin-token") monkeypatch.setenv("PORTFOLIOHUB_READ_TOKENS", "read-token") from app.core.config import get_settings get_settings.cache_clear() app.dependency_overrides.clear() app.dependency_overrides[get_user_service] = lambda: UserService(repo) return TestClient(app) def test_list_users_requires_admin(monkeypatch) -> None: client = _client_with_repo(monkeypatch, FakeUserRepo()) response = client.get("/users", headers={"Authorization": "Bearer read-token"}) app.dependency_overrides.clear() assert response.status_code == 403 assert response.json()["detail"] == "Insufficient permission" def test_list_users_returns_payload(monkeypatch) -> None: repo = FakeUserRepo() repo.create_user("alice", email="alice@example.com") client = _client_with_repo(monkeypatch, repo) response = client.get("/users", headers={"Authorization": "Bearer admin-token"}) app.dependency_overrides.clear() assert response.status_code == 200 payload = response.json() assert len(payload) == 1 assert payload[0]["username"] == "alice" def test_create_user_rejects_duplicate_username(monkeypatch) -> None: repo = FakeUserRepo() repo.create_user("alice") client = _client_with_repo(monkeypatch, repo) response = client.post( "/users", headers={"Authorization": "Bearer admin-token"}, json={"username": "alice", "email": "alice2@example.com"}, ) app.dependency_overrides.clear() assert response.status_code == 409 assert "이미 존재하는 username입니다" in response.json()["detail"] def test_get_update_and_delete_user_flow(monkeypatch) -> None: repo = FakeUserRepo() user = repo.create_user("alice", email="alice@example.com") client = _client_with_repo(monkeypatch, repo) get_response = client.get(f"/users/{user.id}", headers={"Authorization": "Bearer admin-token"}) update_response = client.patch( f"/users/{user.id}", headers={"Authorization": "Bearer admin-token"}, json={"username": "alice-updated"}, ) delete_response = client.delete(f"/users/{user.id}", headers={"Authorization": "Bearer admin-token"}) app.dependency_overrides.clear() assert get_response.status_code == 200 assert get_response.json()["username"] == "alice" assert update_response.status_code == 200 assert update_response.json()["username"] == "alice-updated" assert delete_response.status_code == 204