feat: add service token rename command (#40)
* docs: preserve original issue 5 body * feat: add service token rename command * test: normalize rich option errors
This commit is contained in:
+168
-2
@@ -4,13 +4,25 @@ import json
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import app.cli.commands.health as health_commands
|
||||
from click import unstyle
|
||||
from typer.testing import CliRunner
|
||||
|
||||
import app.cli.commands.health as health_commands
|
||||
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 Asset, AuthToken, BrokerCredential, Daemon, LedgerEntry, Portfolio, PortfolioComment, PortfolioTag, StrategySet, User
|
||||
from app.domain.models import (
|
||||
Asset,
|
||||
AuthToken,
|
||||
BrokerCredential,
|
||||
Daemon,
|
||||
LedgerEntry,
|
||||
Portfolio,
|
||||
PortfolioComment,
|
||||
PortfolioTag,
|
||||
StrategySet,
|
||||
User,
|
||||
)
|
||||
|
||||
|
||||
class FakeRepo:
|
||||
@@ -126,6 +138,15 @@ class FakeRepo:
|
||||
None,
|
||||
)
|
||||
|
||||
def rename_service_token(self, name: str, new_name: str) -> AuthToken:
|
||||
token = self.get_service_token_by_name(name)
|
||||
if token is None:
|
||||
raise ValueError(f"서비스 토큰을 찾을 수 없습니다: {name}")
|
||||
if self.get_service_token_by_name(new_name) is not None:
|
||||
raise ValueError(f"이미 존재하는 서비스 토큰 이름입니다: {new_name}")
|
||||
token.name = new_name
|
||||
return token
|
||||
|
||||
def create_daemon(
|
||||
self,
|
||||
owner_user_id: str,
|
||||
@@ -1055,6 +1076,7 @@ def test_cli_help_exposes_bot_token_commands() -> None:
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "issue" in result.stdout
|
||||
assert "rename" in result.stdout
|
||||
assert "rebind" in result.stdout
|
||||
assert "revoke" in result.stdout
|
||||
|
||||
@@ -1258,6 +1280,150 @@ def test_cli_service_token_revoke_can_abort() -> None:
|
||||
assert repo._auth_tokens[token.id].revoked_at is None
|
||||
|
||||
|
||||
def test_cli_service_token_rename_can_abort_without_mutation() -> None:
|
||||
repo = FakeRepo(_portfolio())
|
||||
user = repo.create_user("alice", permission="read")
|
||||
token = repo.create_auth_token(
|
||||
user.id,
|
||||
name="old-name",
|
||||
token_hash=hash_token("rename-token"),
|
||||
token_prefix=token_prefix("rename-token"),
|
||||
permission="read",
|
||||
token_type="service",
|
||||
)
|
||||
runner = build_runner(repo)
|
||||
|
||||
result = runner.invoke(
|
||||
runner.app,
|
||||
["service-token", "rename", "old-name", "--new-name", "new-name"],
|
||||
input="n\n",
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert repo._auth_tokens[token.id].name == "old-name"
|
||||
|
||||
|
||||
def test_cli_service_token_rename_force_json_has_only_safe_fields() -> None:
|
||||
repo = FakeRepo(_portfolio())
|
||||
user = repo.create_user("alice", permission="read")
|
||||
token = repo.create_auth_token(
|
||||
user.id,
|
||||
name="old-name",
|
||||
token_hash=hash_token("rename-token"),
|
||||
token_prefix=token_prefix("rename-token"),
|
||||
permission="read",
|
||||
token_type="service",
|
||||
)
|
||||
runner = build_runner(repo)
|
||||
|
||||
result = runner.invoke(
|
||||
runner.app,
|
||||
["--format", "json", "service-token", "rename", "old-name", "--new-name", "new-name", "--force"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert json.loads(result.stdout) == {
|
||||
"token_id": token.id,
|
||||
"old_name": "old-name",
|
||||
"new_name": "new-name",
|
||||
"changed": True,
|
||||
}
|
||||
assert repo._auth_tokens[token.id].name == "new-name"
|
||||
|
||||
|
||||
def test_cli_service_token_rename_prompt_accepts_and_table_is_secret_free() -> None:
|
||||
repo = FakeRepo(_portfolio())
|
||||
user = repo.create_user("alice", permission="read")
|
||||
token = repo.create_auth_token(
|
||||
user.id,
|
||||
name="old-name",
|
||||
token_hash=hash_token("rename-secret-value"),
|
||||
token_prefix=token_prefix("rename-secret-value"),
|
||||
permission="read",
|
||||
token_type="service",
|
||||
)
|
||||
runner = build_runner(repo)
|
||||
|
||||
result = runner.invoke(
|
||||
runner.app,
|
||||
["service-token", "rename", "old-name", "--new-name", "new-name"],
|
||||
input="y\n",
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
for field in ("token_id", "old_name", "new_name", "changed"):
|
||||
assert field in result.stdout
|
||||
assert "rename-secret-value" not in result.stdout
|
||||
assert token.token_prefix not in result.stdout
|
||||
assert repo._auth_tokens[token.id].name == "new-name"
|
||||
|
||||
|
||||
def test_cli_service_token_rename_requires_new_name_and_reports_domain_errors() -> None:
|
||||
repo = FakeRepo(_portfolio())
|
||||
user = repo.create_user("alice", permission="read")
|
||||
repo.create_auth_token(
|
||||
user.id,
|
||||
name="source",
|
||||
token_hash=hash_token("rename-source"),
|
||||
token_prefix=token_prefix("rename-source"),
|
||||
permission="read",
|
||||
token_type="service",
|
||||
)
|
||||
repo.create_auth_token(
|
||||
user.id,
|
||||
name="target",
|
||||
token_hash=hash_token("rename-target"),
|
||||
token_prefix=token_prefix("rename-target"),
|
||||
permission="read",
|
||||
token_type="service",
|
||||
)
|
||||
runner = build_runner(repo)
|
||||
|
||||
missing_option = runner.invoke(runner.app, ["service-token", "rename", "source", "--force"])
|
||||
missing_token = runner.invoke(
|
||||
runner.app,
|
||||
["service-token", "rename", "missing", "--new-name", "new", "--force"],
|
||||
)
|
||||
duplicate = runner.invoke(
|
||||
runner.app,
|
||||
["service-token", "rename", "source", "--new-name", "target", "--force"],
|
||||
)
|
||||
|
||||
assert missing_option.exit_code == 2
|
||||
assert "--new-name" in unstyle(missing_option.stderr)
|
||||
assert missing_token.exit_code == 1
|
||||
assert "서비스 토큰을 찾을 수 없습니다: missing" in missing_token.stdout
|
||||
assert duplicate.exit_code == 1
|
||||
assert "이미 존재하는 서비스 토큰 이름입니다: target" in duplicate.stdout
|
||||
|
||||
|
||||
def test_cli_service_token_rename_same_name_short_force_alias() -> None:
|
||||
repo = FakeRepo(_portfolio())
|
||||
user = repo.create_user("alice", permission="read")
|
||||
token = repo.create_auth_token(
|
||||
user.id,
|
||||
name="same-name",
|
||||
token_hash=hash_token("rename-token"),
|
||||
token_prefix=token_prefix("rename-token"),
|
||||
permission="read",
|
||||
token_type="service",
|
||||
)
|
||||
runner = build_runner(repo)
|
||||
|
||||
result = runner.invoke(
|
||||
runner.app,
|
||||
["--format", "json", "service-token", "rename", "same-name", "--new-name", "same-name", "-f"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert json.loads(result.stdout) == {
|
||||
"token_id": token.id,
|
||||
"old_name": "same-name",
|
||||
"new_name": "same-name",
|
||||
"changed": False,
|
||||
}
|
||||
|
||||
|
||||
def test_cli_service_token_revoke_force_succeeds() -> None:
|
||||
repo = FakeRepo(_portfolio())
|
||||
user = repo.create_user("alice", permission="read")
|
||||
|
||||
Reference in New Issue
Block a user