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:
2026-09-19 21:19:39 +09:00
committed by GitHub
parent 996127f215
commit deaaa96c46
20 changed files with 882 additions and 7 deletions
+91 -1
View File
@@ -4,6 +4,7 @@ from dataclasses import dataclass
from datetime import datetime, timezone
from fastapi.testclient import TestClient
import psycopg.errors
from app.core.auth import AuthContext, Permission
from app.core.config import Settings
@@ -96,6 +97,7 @@ class _AuthRepo:
self.tokens: dict[str, AuthToken] = {}
self._daemons: dict[str, object] = {}
self._seq = 1
self.rename_calls = 0
def get_user_by_username(self, username: str):
return self.users.get(username)
@@ -105,10 +107,20 @@ class _AuthRepo:
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":
if token.name == name and token.token_type == "service" and token.revoked_at is None:
return token
return None
def rename_service_token(self, name: str, new_name: str) -> AuthToken:
self.rename_calls += 1
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 psycopg.errors.UniqueViolation("duplicate service-token name")
token.name = new_name
return token
def create_auth_token(
self,
user_id: str,
@@ -231,6 +243,84 @@ def test_revoke_service_token_not_found_raises() -> None:
raise AssertionError("Expected ValueError for missing service token")
def test_rename_service_token_changes_only_name() -> None:
from app.domain.models import User
repo = _AuthRepo()
repo.users["alice"] = User(id="user-1", username="alice", permission="read")
issued = _service(repo).issue_service_token("old-name", "alice")
before = issued.auth_token
protected = (
before.id,
before.user_id,
before.token_prefix,
before.permission,
before.token_type,
before.expires_at,
before.last_used_at,
before.revoked_at,
before.created_at,
before.runtime_id,
before.daemon_id,
)
result = _service(repo).rename_service_token("old-name", "new-name")
assert result.changed is True
assert result.auth_token.name == "new-name"
assert repo.get_service_token_by_name("old-name") is None
assert repo.get_service_token_by_name("new-name") is result.auth_token
assert repo.rename_calls == 1
assert protected == (
result.auth_token.id,
result.auth_token.user_id,
result.auth_token.token_prefix,
result.auth_token.permission,
result.auth_token.token_type,
result.auth_token.expires_at,
result.auth_token.last_used_at,
result.auth_token.revoked_at,
result.auth_token.created_at,
result.auth_token.runtime_id,
result.auth_token.daemon_id,
)
def test_rename_service_token_same_name_is_noop_without_write() -> None:
from app.domain.models import User
repo = _AuthRepo()
repo.users["alice"] = User(id="user-1", username="alice", permission="read")
issued = _service(repo).issue_service_token("unchanged", "alice")
result = _service(repo).rename_service_token("unchanged", "unchanged")
assert result.changed is False
assert result.auth_token is issued.auth_token
assert repo.rename_calls == 0
def test_rename_service_token_missing_and_duplicate_errors() -> None:
from app.domain.models import User
repo = _AuthRepo()
repo.users["alice"] = User(id="user-1", username="alice", permission="read")
service = _service(repo)
service.issue_service_token("source", "alice")
service.issue_service_token("target", "alice")
for old_name, new_name, message in (
("missing", "new", "서비스 토큰을 찾을 수 없습니다: missing"),
("source", "target", "이미 존재하는 서비스 토큰 이름입니다: target"),
):
try:
service.rename_service_token(old_name, new_name)
except ValueError as exc:
assert str(exc) == message
else:
raise AssertionError("Expected rename failure")
# REQ-018: daemon_id forward 보정 (service layer)
def test_issue_service_token_with_daemon_id_stores_it() -> None: