작업 내용 저장

This commit is contained in:
2026-04-23 09:56:08 +00:00
parent 514b49cb98
commit d383c7086f
29 changed files with 1654 additions and 1372 deletions
+55 -72
View File
@@ -8,9 +8,8 @@ from fastapi.testclient import TestClient
from app.core.auth import AuthContext, Permission
from app.core.config import Settings
from app.core.config import get_settings
from app.domain.models import AuthToken, BotPrincipal
from app.domain.models import AuthToken
from app.main import app
from app.schemas.auth import CreateBotTokenRequest
from app.services.auth_service import AuthService
@@ -75,23 +74,19 @@ def test_admin_route_accepts_admin_token(monkeypatch) -> None:
@dataclass
class _AuthRepo:
fail_audit: bool = False
def __post_init__(self) -> None:
self.bot = BotPrincipal(
id="bot-1",
name="bot",
owner_user_id="owner-1",
permission="trade-write",
is_active=True,
)
self.users: dict[str, object] = {}
self.tokens: dict[str, AuthToken] = {}
self._seq = 1
def get_bot_principal(self, bot_principal_id: str) -> BotPrincipal:
if bot_principal_id != self.bot.id:
raise ValueError("봇을 찾을 수 없습니다")
return self.bot
def get_user_by_username(self, username: str):
return self.users.get(username)
def get_service_token_by_name(self, name: str) -> AuthToken | None:
for token in self.tokens.values():
if token.name == name and token.token_type == "service":
return token
return None
def create_auth_token(
self,
@@ -102,7 +97,6 @@ class _AuthRepo:
token_prefix: str,
permission: str,
token_type: str,
bot_principal_id: str | None = None,
expires_at: datetime | None = None,
) -> AuthToken:
token = AuthToken(
@@ -112,7 +106,6 @@ class _AuthRepo:
token_prefix=token_prefix,
permission=permission,
token_type=token_type,
bot_principal_id=bot_principal_id,
expires_at=expires_at,
created_at=datetime.now(timezone.utc),
)
@@ -120,88 +113,78 @@ class _AuthRepo:
self.tokens[token.id] = token
return token
def revoke_bot_auth_token(self, bot_principal_id: str, token_id: str) -> None:
def revoke_auth_token(self, user_id: str, token_id: str) -> None:
token = self.tokens.get(token_id)
if token is None or token.bot_principal_id != bot_principal_id:
if token is None or token.user_id != user_id:
raise ValueError("토큰을 찾을 수 없습니다")
token.revoked_at = datetime.now(timezone.utc)
def record_audit_event(self, payload: dict[str, object]) -> None:
if self.fail_audit:
raise RuntimeError("audit failed")
def _service(repo: _AuthRepo) -> AuthService:
return AuthService(repo, Settings(db_enabled=True, allowed_origins_raw="http://localhost:3000"))
def test_issue_bot_token_defaults_to_365_days() -> None:
def test_issue_service_token_defaults_to_365_days() -> None:
from dataclasses import dataclass as _dc
from app.domain.models import User
repo = _AuthRepo()
repo.users["alice"] = User(id="user-1", username="alice", permission="read")
service = _service(repo)
result = service.issue_bot_token("bot-1", CreateBotTokenRequest(name="cli"))
result = service.issue_service_token("my-token", "alice")
assert result.auth_token.expires_at is not None
assert (result.auth_token.expires_at - datetime.now(timezone.utc)).days >= 364
assert result.auth_token.token_type == "service"
assert result.auth_token.name == "my-token"
def test_issue_owned_bot_token_rejects_no_expiry_for_non_admin() -> None:
def test_issue_service_token_no_expiry() -> None:
from app.domain.models import User
repo = _AuthRepo()
repo.users["alice"] = User(id="user-1", username="alice", permission="read")
service = _service(repo)
result = service.issue_service_token("my-token", "alice", no_expiry=True)
assert result.auth_token.expires_at is None
def test_issue_service_token_unknown_user_raises() -> None:
repo = _AuthRepo()
service = _service(repo)
auth_context = AuthContext(
token="db-token",
permission=Permission.TRADE_WRITE,
user_id="owner-1",
token_source="db",
)
try:
service.issue_owned_bot_token(
auth_context,
"bot-1",
CreateBotTokenRequest(name="cli", no_expiry=True),
)
service.issue_service_token("my-token", "unknown")
except ValueError as exc:
assert "비관리자" in str(exc)
assert "unknown" in str(exc)
else:
raise AssertionError("Expected ValueError for non-admin no-expiry")
raise AssertionError("Expected ValueError for unknown user")
def test_issue_bot_token_rolls_back_when_audit_fails() -> None:
repo = _AuthRepo(fail_audit=True)
def test_revoke_service_token_success() -> None:
from app.domain.models import User
repo = _AuthRepo()
repo.users["alice"] = User(id="user-1", username="alice", permission="read")
service = _service(repo)
issued = service.issue_service_token("my-token", "alice")
revoked_at = service.revoke_service_token("my-token")
assert revoked_at is not None
assert repo.tokens[issued.auth_token.id].revoked_at is not None
def test_revoke_service_token_not_found_raises() -> None:
repo = _AuthRepo()
service = _service(repo)
try:
service.issue_bot_token("bot-1", CreateBotTokenRequest(name="cli"))
service.revoke_service_token("nonexistent")
except ValueError as exc:
assert "발급을 취소" in str(exc)
assert "nonexistent" in str(exc)
else:
raise AssertionError("Expected ValueError on audit failure")
assert len(repo.tokens) == 1
token = next(iter(repo.tokens.values()))
assert token.revoked_at is not None
def test_revoke_bot_token_keeps_revoke_when_audit_fails() -> None:
repo = _AuthRepo(fail_audit=True)
service = _service(repo)
created = repo.create_auth_token(
"bot-1",
name="cli",
token_hash="hash",
token_prefix="prefix",
permission="trade-write",
token_type="bot",
bot_principal_id="bot-1",
)
try:
service.revoke_bot_token("bot-1", created.id)
except ValueError as exc:
assert "토큰은 폐기" in str(exc)
else:
raise AssertionError("Expected ValueError on audit failure")
assert repo.tokens[created.id].revoked_at is not None
raise AssertionError("Expected ValueError for missing service token")