Add admin user management

This commit is contained in:
2026-04-03 12:23:44 +00:00
parent 3d37186fa7
commit 5168e52e4b
18 changed files with 989 additions and 19 deletions
+92 -2
View File
@@ -8,7 +8,7 @@ from typer.testing import CliRunner
from app.cli.main import create_app
from app.core.config import Settings
from app.domain.models import LedgerEntry, Portfolio, StrategySet
from app.domain.models import LedgerEntry, Portfolio, StrategySet, User
class FakeRepo:
@@ -17,6 +17,8 @@ class FakeRepo:
self.entries: list[LedgerEntry] = []
self.strategy_set = StrategySet(id="set-1", name="target_value", config="{}", nick="target_value:99914b932bd3")
self._strategy_sets: dict[str, StrategySet] = {self.strategy_set.id: self.strategy_set}
self._users: dict[str, User] = {}
self._user_seq = 1
self.closed = False
def close(self) -> None:
@@ -28,6 +30,50 @@ class FakeRepo:
def ping(self) -> None:
return None
def get_user(self, user_id: str) -> User:
user = self._users.get(user_id)
if user is None:
raise ValueError("사용자를 찾을 수 없습니다")
return user
def list_users(self) -> list[User]:
return sorted(self._users.values(), key=lambda user: user.username)
def create_user(self, username: str, *, email: str | None = None) -> User:
if any(user.username == username for user in self._users.values()):
raise ValueError(f"이미 존재하는 username입니다: {username}")
if email is not None and any(user.email == email for user in self._users.values()):
raise ValueError(f"이미 존재하는 email입니다: {email}")
now = datetime.now(timezone.utc)
user = User(
id=f"user-{self._user_seq}",
username=username,
email=email,
created_at=now,
updated_at=now,
)
self._user_seq += 1
self._users[user.id] = user
return user
def update_user(self, user_id: str, *, username: str | None = None, email: str | None = None) -> User:
user = self.get_user(user_id)
if username is not None and any(item.id != user_id and item.username == username for item in self._users.values()):
raise ValueError(f"이미 존재하는 username입니다: {username}")
if email is not None and any(item.id != user_id and item.email == email for item in self._users.values()):
raise ValueError(f"이미 존재하는 email입니다: {email}")
if username is not None:
user.username = username
if email is not None:
user.email = email
user.updated_at = datetime.now(timezone.utc)
return user
def delete_user(self, user_id: str) -> None:
if user_id not in self._users:
raise ValueError("사용자를 찾을 수 없습니다")
del self._users[user_id]
def get(self, portfolio_id: str) -> Portfolio:
if self._portfolio is None or self._portfolio.portfolio_id != portfolio_id:
raise ValueError("포트폴리오를 찾을 수 없습니다")
@@ -392,4 +438,48 @@ def test_cli_migrate_db_reports_head(monkeypatch) -> None:
result = runner.invoke(runner.app, ["migrate-db"])
assert result.exit_code == 0
assert "migration_applied: head" in result.stdout
assert "migration_applied: head" in result.stdout
def test_cli_user_list_renders_table() -> None:
repo = FakeRepo()
repo.create_user("alice", email="alice@example.com")
runner = build_runner(repo)
result = runner.invoke(runner.app, ["user", "list"])
assert result.exit_code == 0
assert "Users" in result.stdout
assert "alice" in result.stdout
def test_cli_user_create_supports_json() -> None:
repo = FakeRepo()
runner = build_runner(repo)
result = runner.invoke(
runner.app,
["--format", "json", "user", "create", "--username", "alice", "--email", "alice@example.com", "--force"],
)
assert result.exit_code == 0
payload = json.loads(result.stdout)
assert payload["username"] == "alice"
assert payload["email"] == "alice@example.com"
def test_cli_user_update_and_delete_flow() -> None:
repo = FakeRepo()
user = repo.create_user("alice", email="alice@example.com")
runner = build_runner(repo)
update_result = runner.invoke(
runner.app,
["user", "update", user.id, "--username", "alice-updated", "--force"],
)
delete_result = runner.invoke(runner.app, ["user", "delete", user.id, "--force"])
assert update_result.exit_code == 0
assert "alice-updated" in update_result.stdout
assert delete_result.exit_code == 0
assert f"Deleted user: {user.id}" in delete_result.stdout