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:
+91
-1
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user