fix(daemons): block owner transfer while assigned
This commit is contained in:
@@ -9,6 +9,7 @@ import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.core.config import Settings, get_settings
|
||||
from app.domain.errors import DaemonInUseError
|
||||
from app.domain.models import BrokerCredential, Daemon, TokenPrincipal, User
|
||||
from app.main import create_app
|
||||
from app.repositories.postgres import PostgresPortfolioRepository
|
||||
@@ -212,6 +213,33 @@ def test_admin_patch_owner_invalidates_credentials() -> None:
|
||||
mock_repo.update_daemon_owner.assert_called_once_with(DAEMON_ID, owner_user_id=OWNER_B_ID)
|
||||
|
||||
|
||||
def test_admin_patch_referenced_daemon_owner_returns_409() -> None:
|
||||
"""Issue #27: assigned daemon owner transfer fails without a partial mutation."""
|
||||
settings = _make_settings()
|
||||
principal = _make_admin_principal()
|
||||
daemon_before = _make_daemon(owner_user_id=OWNER_A_ID)
|
||||
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.get_token_principal.return_value = principal
|
||||
mock_repo.get_daemon_by_id.return_value = daemon_before
|
||||
mock_repo.update_daemon_owner.side_effect = DaemonInUseError(
|
||||
"배정된 포트폴리오가 있는 daemon의 소유자는 변경할 수 없습니다"
|
||||
)
|
||||
|
||||
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo), \
|
||||
patch("app.api.v1_admin_daemons.get_portfolio_repository_from_settings", return_value=mock_repo):
|
||||
client = _make_client(settings, mock_repo)
|
||||
resp = client.patch(
|
||||
f"/v1/admin/daemons/{DAEMON_ID}",
|
||||
json={"owner_user_id": OWNER_B_ID},
|
||||
headers=_auth_header(),
|
||||
)
|
||||
|
||||
assert resp.status_code == 409
|
||||
mock_repo.update_daemon_owner.assert_called_once_with(DAEMON_ID, owner_user_id=OWNER_B_ID)
|
||||
mock_repo.update_daemon.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# REQ-053 AC-5: owner 변경 후 이전 owner의 /v1/user_info → broker=null
|
||||
# (verified via update_daemon_owner deleting credentials; AC-4 covers the mechanism)
|
||||
@@ -489,7 +517,9 @@ def _make_mock_conn_for_owner_update(*, fail_on_update: bool = False):
|
||||
def execute(self, sql: str, params=None) -> None:
|
||||
sql_stripped = sql.strip()
|
||||
executed_sqls.append(sql_stripped)
|
||||
if sql_stripped.startswith("SELECT"):
|
||||
if "FROM portfolios" in sql_stripped:
|
||||
self._rows = []
|
||||
elif sql_stripped.startswith("SELECT"):
|
||||
# daemon exists
|
||||
self._rows = [("daemon-001",)]
|
||||
elif sql_stripped.startswith("DELETE"):
|
||||
@@ -717,6 +747,8 @@ def test_update_daemon_owner_both_ops_in_single_transaction() -> None:
|
||||
|
||||
def fetchone(self):
|
||||
sql = executed_sqls[-1] if executed_sqls else ""
|
||||
if "FROM portfolios" in sql:
|
||||
return None
|
||||
if sql.startswith("SELECT"):
|
||||
return ("daemon-001",)
|
||||
if sql.startswith("UPDATE"):
|
||||
|
||||
Reference in New Issue
Block a user