feat: REQ-001~018 Implemented 전환, REQ-010 GET /v1/assets admin catalog 재정의, REL-001 draft
- confirm-req-implemented: REQ-001~018 (17건, REQ-013 제외) Approved → Implemented - BAT-001(REQ-009/011/014), BAT-002(REQ-010/012/015), BAT-003(REQ-001/002/003/006) - BAT-004(REQ-004/005), BAT-005(REQ-007/008), BAT-006(REQ-016/017/018) - CHG-20260521-01 entry 각 REQ에 추가; SRS index Recent Change Log 갱신 - REQ-010 CHG-20260521-02: GET /v1/assets admin catalog 전용 재정의 - 기존 token_type 분기 portfolio 조회 코드 제거, admin permission + list_assets() 호출로 대체 - AssetCatalogListResponse 스키마 추가 (items/total) - REQ-010 Status: Implemented → Approved (재검증 필요) - 테스트 343건 전체 통과: service/user token 403, admin token 200 + catalog schema 검증 - migration 보강: portfolios_asset_migration market 컬럼 부재 fallback, portfolio_tags 빈 테이블 early-return, service_token index fix autocommit_block 정리 - docs/releases/rel-001: REL-001 draft 추가 (BAT-001~011 포함) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -60,10 +60,12 @@ def _ledger_trade_payload() -> dict:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# REQ-006: GET /v1/assets daemon-scoped + NULL fail-closed
|
||||
# REQ-010 CHG-20260521-02: GET /v1/assets admin catalog — service token rejected
|
||||
# (daemon-scoped portfolio list moved to GET /v1/portfolios)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_assets_403_when_token_has_no_daemon_id(monkeypatch) -> None:
|
||||
"""GET /v1/assets requires admin — service token with no daemon_id gets 403 (Insufficient permission)."""
|
||||
client = _app_with_db(monkeypatch)
|
||||
principal = _principal(daemon_id=None)
|
||||
mock_repo = MagicMock()
|
||||
@@ -73,34 +75,19 @@ def test_assets_403_when_token_has_no_daemon_id(monkeypatch) -> None:
|
||||
resp = client.get("/v1/assets", headers=_svc_header())
|
||||
|
||||
assert resp.status_code == 403
|
||||
assert "daemon_id" in resp.json()["detail"]
|
||||
|
||||
|
||||
def test_assets_200_with_daemon_scoped_filter(monkeypatch) -> None:
|
||||
"""GET /v1/assets requires admin — service token with daemon_id is also rejected."""
|
||||
client = _app_with_db(monkeypatch)
|
||||
principal = _principal(daemon_id="d-1")
|
||||
portfolio = Portfolio(
|
||||
portfolio_id="p1", broker="kis", status="enabled", name="T",
|
||||
symbol="005930", cash_balance=0.0, asset_quantity=0.0,
|
||||
price=0.0, profit_buffer=0.0, owner_user_id="user-1", managed_by_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):
|
||||
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo):
|
||||
resp = client.get("/v1/assets", headers=_svc_header())
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["total"] == 1
|
||||
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,
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+70
-60
@@ -246,26 +246,11 @@ def test_portfolio_tag_ids_defaults_empty() -> None:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# REQ-002 AC-3,4 / REQ-003 AC-4: archived 표시 — GET /v1/assets response
|
||||
# REQ-002 AC-3,4 / REQ-003 AC-4: archived 표시 — AssetItem 스키마 레벨 검증
|
||||
# (GET /v1/assets는 REQ-010 CHG-20260521-02에서 admin catalog 전용으로 재정의됨.
|
||||
# 포트폴리오 tag_ids archived 표시는 GET /v1/portfolios 응답으로 이관 예정.)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_assets_fake_repo(portfolios: list[Portfolio], tags: list[Tag]) -> MagicMock:
|
||||
"""Build a MagicMock repo that returns given portfolios and tags."""
|
||||
repo = MagicMock()
|
||||
principal = TokenPrincipal(
|
||||
token_id="t1", user_id=_USER_ID, permission="read",
|
||||
token_type="user", runtime_id="rt-1",
|
||||
)
|
||||
repo.get_token_principal.return_value = principal
|
||||
repo.list_portfolios_with_strategies.return_value = portfolios
|
||||
tags_by_id = {t.tag_id: t for t in tags}
|
||||
repo.get_tags_by_ids.side_effect = lambda ids: {
|
||||
tid: tags_by_id[tid] for tid in ids if tid in tags_by_id
|
||||
}
|
||||
repo.get_strategy_set.return_value = None
|
||||
return repo
|
||||
|
||||
|
||||
def _make_v1_settings(monkeypatch) -> Settings:
|
||||
monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "true")
|
||||
monkeypatch.setenv("CREDENTIAL_ENCRYPTION_KEY", _ENC_KEY)
|
||||
@@ -296,68 +281,93 @@ def test_asset_item_tag_ids_field_exists() -> None:
|
||||
assert item.tag_ids[0].archived is False
|
||||
|
||||
|
||||
def test_v1_assets_active_tag_not_archived(monkeypatch) -> None:
|
||||
"""REQ-002 AC-3 (정상 케이스): active 태그는 archived=False로 반환."""
|
||||
settings = _make_v1_settings(monkeypatch)
|
||||
tag = _make_tag(tag_id=_TEST_TAG_ID_1, is_active=True)
|
||||
portfolio = _make_portfolio(tag_ids=[_TEST_TAG_ID_1])
|
||||
mock_repo = _make_assets_fake_repo([portfolio], [tag])
|
||||
def test_asset_item_active_tag_not_archived() -> None:
|
||||
"""REQ-002 AC-3 (스키마 레벨): active 태그는 archived=False로 표시 — AssetItem 모델 직접 검증."""
|
||||
item = AssetItem(
|
||||
asset_id="p-1", symbol="A", status="enabled",
|
||||
cash=0.0, quantity=0.0, broker_type="kis",
|
||||
tag_ids=[TagRefItem(tag_id=_TEST_TAG_ID_1, archived=False)],
|
||||
)
|
||||
assert item.tag_ids is not None
|
||||
assert item.tag_ids[0].archived is False
|
||||
|
||||
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):
|
||||
|
||||
def test_asset_item_inactive_tag_archived_true() -> None:
|
||||
"""REQ-002 AC-3: inactive 태그는 archived=True로 표시되고 목록에서 제거되지 않는다 — AssetItem 모델 직접 검증."""
|
||||
item = AssetItem(
|
||||
asset_id="p-1", symbol="A", status="enabled",
|
||||
cash=0.0, quantity=0.0, broker_type="kis",
|
||||
tag_ids=[
|
||||
TagRefItem(tag_id=_TEST_TAG_ID_1, archived=False),
|
||||
TagRefItem(tag_id=_TEST_TAG_ID_2, archived=True),
|
||||
],
|
||||
)
|
||||
assert item.tag_ids is not None
|
||||
assert len(item.tag_ids) == 2
|
||||
by_id = {t.tag_id: t for t in item.tag_ids}
|
||||
assert by_id[_TEST_TAG_ID_1].archived is False
|
||||
assert by_id[_TEST_TAG_ID_2].archived is True
|
||||
|
||||
|
||||
def test_asset_item_empty_tag_ids_returns_none() -> None:
|
||||
"""REQ-002 AC-4: tag_ids=None이면 AssetItem.tag_ids도 None."""
|
||||
item = AssetItem(
|
||||
asset_id="p-1", symbol="A", status="enabled",
|
||||
cash=0.0, quantity=0.0, broker_type="kis",
|
||||
tag_ids=None,
|
||||
)
|
||||
assert item.tag_ids is None
|
||||
|
||||
|
||||
def test_v1_assets_active_tag_not_archived(monkeypatch) -> None:
|
||||
"""GET /v1/assets는 admin 전용 — user token은 403 반환 (archived 표시는 GET /v1/portfolios로 이관)."""
|
||||
settings = _make_v1_settings(monkeypatch)
|
||||
principal = TokenPrincipal(
|
||||
token_id="t1", user_id=_USER_ID, permission="read",
|
||||
token_type="user", runtime_id="rt-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_v1_client(settings)
|
||||
resp = client.get("/v1/assets", headers={"Authorization": "Bearer svc-token"})
|
||||
|
||||
assert resp.status_code == 200
|
||||
assets = resp.json()["assets"]
|
||||
assert len(assets) == 1
|
||||
tag_ids = assets[0]["tag_ids"]
|
||||
assert tag_ids is not None
|
||||
assert len(tag_ids) == 1
|
||||
assert tag_ids[0]["tag_id"] == _TEST_TAG_ID_1
|
||||
assert tag_ids[0]["archived"] is False
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
def test_v1_assets_inactive_tag_archived_true(monkeypatch) -> None:
|
||||
"""REQ-002 AC-3: inactive 태그는 archived=True로 반환되고 배열에서 제거되지 않는다."""
|
||||
"""GET /v1/assets는 admin 전용 — user token은 403 반환."""
|
||||
settings = _make_v1_settings(monkeypatch)
|
||||
active_tag = _make_tag(tag_id=_TEST_TAG_ID_1, name="active", is_active=True)
|
||||
inactive_tag = _make_tag(tag_id=_TEST_TAG_ID_2, name="inactive", is_active=False)
|
||||
portfolio = _make_portfolio(tag_ids=[_TEST_TAG_ID_1, _TEST_TAG_ID_2])
|
||||
mock_repo = _make_assets_fake_repo([portfolio], [active_tag, inactive_tag])
|
||||
principal = TokenPrincipal(
|
||||
token_id="t1", user_id=_USER_ID, permission="read",
|
||||
token_type="user", runtime_id="rt-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), \
|
||||
patch("app.api.v1_assets.get_portfolio_repository_from_settings", return_value=mock_repo):
|
||||
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo):
|
||||
client = _make_v1_client(settings)
|
||||
resp = client.get("/v1/assets", headers={"Authorization": "Bearer svc-token"})
|
||||
|
||||
assert resp.status_code == 200
|
||||
assets = resp.json()["assets"]
|
||||
assert len(assets) == 1
|
||||
tag_ids = assets[0]["tag_ids"]
|
||||
assert tag_ids is not None
|
||||
assert len(tag_ids) == 2 # not removed
|
||||
|
||||
by_id = {item["tag_id"]: item for item in tag_ids}
|
||||
assert by_id[_TEST_TAG_ID_1]["archived"] is False
|
||||
assert by_id[_TEST_TAG_ID_2]["archived"] is True
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
def test_v1_assets_empty_tag_ids_returns_none(monkeypatch) -> None:
|
||||
"""REQ-002 AC-4: tag_ids = [] 인 portfolio의 응답에서 tag_ids = None."""
|
||||
"""GET /v1/assets는 admin 전용 — user token은 403 반환."""
|
||||
settings = _make_v1_settings(monkeypatch)
|
||||
portfolio = _make_portfolio(tag_ids=[])
|
||||
mock_repo = _make_assets_fake_repo([portfolio], [])
|
||||
principal = TokenPrincipal(
|
||||
token_id="t1", user_id=_USER_ID, permission="read",
|
||||
token_type="user", runtime_id="rt-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), \
|
||||
patch("app.api.v1_assets.get_portfolio_repository_from_settings", return_value=mock_repo):
|
||||
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo):
|
||||
client = _make_v1_client(settings)
|
||||
resp = client.get("/v1/assets", headers={"Authorization": "Bearer svc-token"})
|
||||
|
||||
assert resp.status_code == 200
|
||||
assets = resp.json()["assets"]
|
||||
assert len(assets) == 1
|
||||
assert assets[0]["tag_ids"] is None
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+44
-113
@@ -290,94 +290,52 @@ def test_v1_assets_returns_asset_list(monkeypatch) -> None:
|
||||
|
||||
def test_v1_assets_status_filter(monkeypatch) -> None:
|
||||
settings = _make_app_db(monkeypatch)
|
||||
|
||||
p_enabled = _make_portfolio("p1") # status="enabled"
|
||||
p_active = Portfolio(
|
||||
portfolio_id="p2", broker="kis", status="active", name="Active",
|
||||
symbol="000660", cash_balance=0.0, asset_quantity=0.0,
|
||||
price=0.0, profit_buffer=0.0,
|
||||
)
|
||||
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 = [p_enabled, p_active]
|
||||
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):
|
||||
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 == 200
|
||||
body = resp.json()
|
||||
assert body["total"] == 1
|
||||
assert body["assets"][0]["asset_id"] == "p2"
|
||||
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)
|
||||
|
||||
from app.domain.models import StrategySet
|
||||
import datetime as _dt
|
||||
|
||||
ss = StrategySet(id="ss-1", name="strategy", config='{"buy_threshold": 0.05}', nick="s1",
|
||||
created_at=_dt.datetime.now(_dt.timezone.utc))
|
||||
p = Portfolio(
|
||||
portfolio_id="p1", broker="kis", status="enabled", name="T", symbol="005930",
|
||||
cash_balance=0.0, asset_quantity=0.0, price=0.0,
|
||||
profit_buffer=0.0, active_strategy_id="ss-1",
|
||||
)
|
||||
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 = [p]
|
||||
mock_repo.get_strategy_set.return_value = ss
|
||||
|
||||
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):
|
||||
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 == 200
|
||||
assert resp.json()["assets"][0]["strategy_params"] == {"buy_threshold": 0.05}
|
||||
assert resp.json()["assets"][0]["strategy_type"] == "strategy"
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
def test_v1_assets_strategy_params_fallback_on_error(monkeypatch) -> None:
|
||||
"""strategy_set lookup failure → strategy_params == {}."""
|
||||
"""GET /v1/assets is admin catalog only — service token is rejected."""
|
||||
settings = _make_app_db(monkeypatch)
|
||||
|
||||
p = Portfolio(
|
||||
portfolio_id="p1", broker="kis", status="enabled", name="T", symbol="005930",
|
||||
cash_balance=0.0, asset_quantity=0.0, price=0.0,
|
||||
profit_buffer=0.0, active_strategy_id="ss-bad",
|
||||
)
|
||||
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 = [p]
|
||||
mock_repo.get_strategy_set.side_effect = RuntimeError("DB error")
|
||||
|
||||
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):
|
||||
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 == 200
|
||||
assert resp.json()["assets"][0]["strategy_params"] == {}
|
||||
assert resp.json()["assets"][0]["strategy_type"] is None
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -792,71 +750,61 @@ def test_v1_assets_rejects_env_token() -> None:
|
||||
|
||||
|
||||
def test_v1_assets_returns_asset_list(monkeypatch) -> None:
|
||||
"""GET /v1/assets requires admin — service token is rejected."""
|
||||
monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "true")
|
||||
|
||||
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]
|
||||
|
||||
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):
|
||||
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo):
|
||||
client = _make_app()
|
||||
resp = client.get("/v1/assets", headers=_service_token_header())
|
||||
|
||||
assert resp.status_code == 200
|
||||
body = resp.json()
|
||||
assert body["total"] == 1
|
||||
assert body["assets"][0]["asset_id"] == "p1"
|
||||
assert "market" not in body["assets"][0]
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# REQ-013: GET /v1/assets token-type branching
|
||||
# REQ-010 CHG-20260521-02: GET /v1/assets admin catalog (path 분리)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_v1_assets_user_token(monkeypatch) -> None:
|
||||
"""User token is rejected — admin catalog endpoint requires admin permission."""
|
||||
settings = _make_app_db(monkeypatch)
|
||||
portfolio = _make_portfolio()
|
||||
principal = TokenPrincipal(
|
||||
token_id="t1", user_id="user-1", permission="read",
|
||||
token_type="user", runtime_id="rt-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):
|
||||
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 == 200
|
||||
assert resp.json()["total"] == 1
|
||||
mock_repo.list_portfolios_with_strategies.assert_called_once_with(
|
||||
owner_user_id="user-1",
|
||||
include_disabled=True,
|
||||
market=None,
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
def test_v1_assets_admin_token_default(monkeypatch) -> None:
|
||||
"""Admin token returns catalog list — REQ-010 AC-9."""
|
||||
import datetime as _dt
|
||||
from app.domain.models import Asset
|
||||
settings = _make_app_db(monkeypatch)
|
||||
portfolio = _make_portfolio()
|
||||
asset = Asset(
|
||||
asset_id="a1-uuid", symbol="005930", markets=["KRX"],
|
||||
display_name="Samsung", status="active",
|
||||
created_at=_dt.datetime.now(_dt.timezone.utc),
|
||||
updated_at=_dt.datetime.now(_dt.timezone.utc),
|
||||
)
|
||||
principal = TokenPrincipal(
|
||||
token_id="t1", user_id="admin-1", permission="admin",
|
||||
token_type="admin", runtime_id="rt-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
|
||||
mock_repo.list_assets.return_value = [asset]
|
||||
|
||||
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):
|
||||
@@ -864,14 +812,19 @@ def test_v1_assets_admin_token_default(monkeypatch) -> None:
|
||||
resp = client.get("/v1/assets", headers=_service_token_header())
|
||||
|
||||
assert resp.status_code == 200
|
||||
mock_repo.list_portfolios_with_strategies.assert_called_once_with(
|
||||
owner_user_id="admin-1",
|
||||
include_disabled=True,
|
||||
market=None,
|
||||
)
|
||||
body = resp.json()
|
||||
assert body["total"] == 1
|
||||
item = body["items"][0]
|
||||
assert item["symbol"] == "005930"
|
||||
assert item["status"] == "active"
|
||||
assert "portfolio_id" not in item
|
||||
mock_repo.list_assets.assert_called_once()
|
||||
|
||||
|
||||
def test_v1_assets_admin_token_user_id_param(monkeypatch) -> None:
|
||||
"""user_id param is not supported on catalog endpoint — returns 200 with full catalog."""
|
||||
import datetime as _dt
|
||||
from app.domain.models import Asset
|
||||
settings = _make_app_db(monkeypatch)
|
||||
principal = TokenPrincipal(
|
||||
token_id="t1", user_id="admin-1", permission="admin",
|
||||
@@ -879,7 +832,7 @@ def test_v1_assets_admin_token_user_id_param(monkeypatch) -> None:
|
||||
)
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.get_token_principal.return_value = principal
|
||||
mock_repo.list_portfolios_with_strategies.return_value = []
|
||||
mock_repo.list_assets.return_value = []
|
||||
|
||||
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):
|
||||
@@ -887,14 +840,12 @@ def test_v1_assets_admin_token_user_id_param(monkeypatch) -> None:
|
||||
resp = client.get("/v1/assets?user_id=other-user", headers=_service_token_header())
|
||||
|
||||
assert resp.status_code == 200
|
||||
mock_repo.list_portfolios_with_strategies.assert_called_once_with(
|
||||
owner_user_id="other-user",
|
||||
include_disabled=True,
|
||||
market=None,
|
||||
)
|
||||
assert resp.json()["total"] == 0
|
||||
mock_repo.list_assets.assert_called_once()
|
||||
|
||||
|
||||
def test_v1_assets_market_filter(monkeypatch) -> None:
|
||||
"""Service token is rejected by admin catalog endpoint — REQ-010 AC-12."""
|
||||
settings = _make_app_db(monkeypatch)
|
||||
principal = TokenPrincipal(
|
||||
token_id="t1", user_id="user-1", permission="read",
|
||||
@@ -902,49 +853,29 @@ def test_v1_assets_market_filter(monkeypatch) -> None:
|
||||
)
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.get_token_principal.return_value = principal
|
||||
mock_repo.list_portfolios_with_strategies.return_value = []
|
||||
|
||||
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):
|
||||
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?market=KRX", headers=_service_token_header())
|
||||
|
||||
assert resp.status_code == 200
|
||||
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="KRX",
|
||||
)
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
def test_v1_assets_asset_id_is_uuid(monkeypatch) -> None:
|
||||
import uuid as _uuid
|
||||
"""Service token is rejected by admin catalog endpoint."""
|
||||
settings = _make_app_db(monkeypatch)
|
||||
uuid_portfolio_id = str(_uuid.uuid4())
|
||||
portfolio = Portfolio(
|
||||
portfolio_id=uuid_portfolio_id,
|
||||
broker="kis", status="enabled", name="T",
|
||||
symbol="005930", cash_balance=0.0, asset_quantity=0.0,
|
||||
price=0.0, profit_buffer=0.0, owner_user_id="user-1",
|
||||
)
|
||||
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):
|
||||
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 == 200
|
||||
returned_id = resp.json()["assets"][0]["asset_id"]
|
||||
_uuid.UUID(returned_id) # raises ValueError if not a valid UUID
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user