작업 내용 저장

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
+29 -42
View File
@@ -10,7 +10,7 @@ from typer.testing import CliRunner
from app.cli.main import create_app
from app.core.config import Settings
from app.core.security import hash_token, token_prefix
from app.domain.models import AuthToken, BotPrincipal, LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet, User
from app.domain.models import AuthToken, LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet, User
class FakeRepo:
@@ -23,15 +23,6 @@ class FakeRepo:
self._user_seq = 1
self._comments: dict[str, PortfolioComment] = {}
self._tags: dict[str, PortfolioTag] = {}
self._bot_principals: dict[str, BotPrincipal] = {
"bot-1": BotPrincipal(
id="bot-1",
name="cli-bot",
owner_user_id="owner-1",
permission="trade-write",
is_active=True,
)
}
self._auth_tokens: dict[str, AuthToken] = {}
self._token_seq = 1
self.closed = False
@@ -115,11 +106,11 @@ class FakeRepo:
raise ValueError("사용자를 찾을 수 없습니다")
del self._users[user_id]
def get_bot_principal(self, bot_principal_id: str) -> BotPrincipal:
bot = self._bot_principals.get(bot_principal_id)
if bot is None:
raise ValueError("봇을 찾을 수 없습니다")
return bot
def get_user_by_username(self, username: str):
return next((u for u in self._users.values() if u.username == username), None)
def get_service_token_by_name(self, name: str) -> AuthToken | None:
return next((t for t in self._auth_tokens.values() if t.name == name and t.token_type == "service"), None)
def create_auth_token(
self,
@@ -130,7 +121,6 @@ class FakeRepo:
token_prefix: str,
permission: str,
token_type: str,
bot_principal_id: str | None = None,
expires_at: datetime | None = None,
) -> AuthToken:
token = AuthToken(
@@ -140,7 +130,6 @@ class FakeRepo:
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),
)
@@ -148,16 +137,12 @@ class FakeRepo:
self._auth_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._auth_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 payload.get("force_fail"):
raise RuntimeError("audit failure")
def get(self, portfolio_id: str) -> Portfolio:
if self._portfolio is None or self._portfolio.portfolio_id != portfolio_id:
raise ValueError("포트폴리오를 찾을 수 없습니다")
@@ -837,25 +822,26 @@ def test_cli_help_exposes_bot_token_commands() -> None:
assert "revoke" in result.stdout
def test_cli_bot_token_issue_supports_json() -> None:
def test_cli_service_token_issue_supports_json() -> None:
repo = FakeRepo(_portfolio())
user = repo.create_user("alice", permission="read")
runner = build_runner(repo)
result = runner.invoke(
runner.app,
["--format", "json", "service-token", "issue", "bot-1", "--name", "nightly"],
["--format", "json", "service-token", "issue", "nightly", "--user", "alice"],
)
assert result.exit_code == 0
payload = json.loads(result.stdout)
assert payload["bot_id"] == "bot-1"
assert payload["name"] == "nightly"
assert payload["revoked"] is False
assert payload["expires_at"] is not None
assert payload["token"].endswith("...") is False
def test_cli_bot_token_issue_rejects_conflicting_expiry_options() -> None:
def test_cli_service_token_issue_rejects_conflicting_expiry_options() -> None:
repo = FakeRepo(_portfolio())
repo.create_user("alice", permission="read")
runner = build_runner(repo)
result = runner.invoke(
@@ -863,7 +849,8 @@ def test_cli_bot_token_issue_rejects_conflicting_expiry_options() -> None:
[
"service-token",
"issue",
"bot-1",
"nightly",
"--user", "alice",
"--ttl-days",
"7",
"--no-expiry",
@@ -874,22 +861,22 @@ def test_cli_bot_token_issue_rejects_conflicting_expiry_options() -> None:
assert "동시에 사용할 수 없습니다" in result.stdout
def test_cli_bot_token_revoke_can_abort() -> None:
def test_cli_service_token_revoke_can_abort() -> None:
repo = FakeRepo(_portfolio())
user = repo.create_user("alice", permission="read")
token = repo.create_auth_token(
"bot-1",
user.id,
name="to-revoke",
token_hash=hash_token("token-value"),
token_prefix=token_prefix("token-value"),
permission="trade-write",
token_type="bot",
bot_principal_id="bot-1",
permission="read",
token_type="service",
)
runner = build_runner(repo)
result = runner.invoke(
runner.app,
["service-token", "revoke", "bot-1", token.id],
["service-token", "revoke", "to-revoke"],
input="n\n",
)
@@ -897,25 +884,25 @@ def test_cli_bot_token_revoke_can_abort() -> None:
assert repo._auth_tokens[token.id].revoked_at is None
def test_cli_bot_token_revoke_force_succeeds() -> None:
def test_cli_service_token_revoke_force_succeeds() -> None:
repo = FakeRepo(_portfolio())
user = repo.create_user("alice", permission="read")
token = repo.create_auth_token(
"bot-1",
user.id,
name="to-revoke",
token_hash=hash_token("token-value"),
token_prefix=token_prefix("token-value"),
permission="trade-write",
token_type="bot",
bot_principal_id="bot-1",
permission="read",
token_type="service",
)
runner = build_runner(repo)
result = runner.invoke(
runner.app,
["--format", "json", "service-token", "revoke", "bot-1", token.id, "--force"],
["--format", "json", "service-token", "revoke", "to-revoke", "--force"],
)
assert result.exit_code == 0
payload = json.loads(result.stdout)
assert payload["token_id"] == token.id
assert payload["name"] == "to-revoke"
assert payload["revoked"] is True