feat: REQ-028 add display_name to GET /v1/portfolios response

- Portfolio domain model + PortfolioItem schema에 display_name 필드 추가
- _SELECT_PORTFOLIO에 a.display_name JOIN 보강, _portfolio_to_item 매퍼 전달
- REQ-028 CHG-20260601-01/02: Implemented → Approved → Implemented
  - AC-13 신설 (display_name == joined assets.display_name)
- BAT-009 verification.md: AC-13 매핑 + augmentation history 누적
- REL-001 verification checklist 재실행 evidence 갱신 (368 PASSED, 로컬 smoke 5건 통과)
- tests/test_v1.py: dead 중복 GET /v1/assets 섹션 제거 (두 번째 동명 정의가 첫 번째를 override하던 잔재)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 14:00:59 +00:00
co-authored by Claude Opus 4.7
parent 136ae16885
commit 84de34c301
10 changed files with 112 additions and 151 deletions
-129
View File
@@ -209,135 +209,6 @@ def test_auth_issue_service_token_generates_distinct_runtime_ids_per_token(monke
assert first.json()["runtime_id"] != second.json()["runtime_id"]
# ---------------------------------------------------------------------------
# GET /v1/assets
# ---------------------------------------------------------------------------
def _make_portfolio(portfolio_id: str = "p1") -> Portfolio:
return Portfolio(
portfolio_id=portfolio_id,
broker="kis",
status="enabled",
name="Test",
symbol="005930",
tags=["tag1"],
cash_balance=1000.0,
asset_quantity=10.0,
price=50000.0,
profit_buffer=0.0,
active_strategy_id=None,
avabile_strategies=None,
owner_user_id="user-1",
managed_by_daemon_id="d-1",
)
def test_v1_assets_requires_service_token(monkeypatch) -> None:
monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "false")
settings = _make_settings(db_enabled=False)
client = _make_app(settings)
resp = client.get("/v1/assets")
assert resp.status_code == 401
def test_v1_assets_rejects_env_token() -> None:
from app.core.config import get_settings
settings = _make_settings(db_enabled=False, read_tokens_raw="env-token")
app = create_app(settings_factory=lambda: settings)
app.dependency_overrides[get_settings] = lambda: settings
client = TestClient(app)
resp = client.get("/v1/assets", headers=_service_token_header("env-token"))
assert resp.status_code == 403
def test_v1_assets_returns_asset_list(monkeypatch) -> None:
settings = _make_app_db(monkeypatch)
portfolio = _make_portfolio()
principal = TokenPrincipal(
token_id="t1", user_id="user-1", permission="read",
token_type="service", runtime_id="rt-1", daemon_id="d-1"
)
mock_repo = MagicMock()
mock_repo.get_token_principal.return_value = principal
mock_repo.list_portfolios_with_strategies.return_value = [portfolio]
mock_repo.get_strategy_set.return_value = None
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo), \
patch("app.api.v1_assets.get_portfolio_repository_from_settings", return_value=mock_repo):
client = _make_app(_make_settings(credential_encryption_key=_TEST_ENC_KEY))
resp = client.get("/v1/assets", headers=_service_token_header())
assert resp.status_code == 200
body = resp.json()
assert body["total"] == 1
item = body["assets"][0]
assert item["asset_id"] == "p1"
assert "market" not in item
assert item["broker_type"] == "kis"
assert item["cash"] == 1000.0
assert item["quantity"] == 10.0
assert item["strategy_params"] == {}
assert item["strategy_type"] is None
mock_repo.list_portfolios_with_strategies.assert_called_once_with(
owner_user_id="user-1",
include_disabled=False,
managed_by_daemon_id="d-1",
market=None,
)
def test_v1_assets_status_filter(monkeypatch) -> None:
settings = _make_app_db(monkeypatch)
principal = TokenPrincipal(
token_id="t1", user_id="user-1", permission="read",
token_type="service", runtime_id="rt-1", daemon_id="d-1"
)
mock_repo = MagicMock()
mock_repo.get_token_principal.return_value = principal
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo):
client = _make_app(_make_settings(credential_encryption_key=_TEST_ENC_KEY))
resp = client.get("/v1/assets?status=active", headers=_service_token_header())
assert resp.status_code == 403
def test_v1_assets_strategy_params(monkeypatch) -> None:
"""GET /v1/assets is admin catalog only — service token is rejected (strategy data is on GET /v1/portfolios)."""
settings = _make_app_db(monkeypatch)
principal = TokenPrincipal(
token_id="t1", user_id="user-1", permission="read",
token_type="service", runtime_id="rt-1", daemon_id="d-1"
)
mock_repo = MagicMock()
mock_repo.get_token_principal.return_value = principal
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo):
client = _make_app(_make_settings(credential_encryption_key=_TEST_ENC_KEY))
resp = client.get("/v1/assets", headers=_service_token_header())
assert resp.status_code == 403
def test_v1_assets_strategy_params_fallback_on_error(monkeypatch) -> None:
"""GET /v1/assets is admin catalog only — service token is rejected."""
settings = _make_app_db(monkeypatch)
principal = TokenPrincipal(
token_id="t1", user_id="user-1", permission="read",
token_type="service", runtime_id="rt-1", daemon_id="d-1"
)
mock_repo = MagicMock()
mock_repo.get_token_principal.return_value = principal
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo):
client = _make_app(_make_settings(credential_encryption_key=_TEST_ENC_KEY))
resp = client.get("/v1/assets", headers=_service_token_header())
assert resp.status_code == 403
# ---------------------------------------------------------------------------
# GET /v1/user_info
# ---------------------------------------------------------------------------
+10 -2
View File
@@ -26,8 +26,14 @@ def _make_settings(**kwargs) -> Settings:
return Settings(allowed_origins_raw="http://localhost:3000", **kwargs)
def _make_asset(markets: list[str] | None = None) -> Asset:
return Asset(asset_id=_ASSET_UUID, symbol="005930", markets=markets or ["KRX"], status="active")
def _make_asset(markets: list[str] | None = None, display_name: str | None = "삼성전자") -> Asset:
return Asset(
asset_id=_ASSET_UUID,
symbol="005930",
markets=markets or ["KRX"],
display_name=display_name,
status="active",
)
def _make_portfolio(
@@ -35,6 +41,7 @@ def _make_portfolio(
owner: str = _USER_ID,
status: str = "enabled",
daemon_id: str | None = None,
display_name: str | None = "삼성전자",
) -> Portfolio:
return Portfolio(
portfolio_id=portfolio_id,
@@ -42,6 +49,7 @@ def _make_portfolio(
status=status,
name=f"p-{portfolio_id[:4]}",
symbol="005930",
display_name=display_name,
asset_id=_ASSET_UUID,
owner_user_id=owner,
cash_balance=0.0,