feat(bat-019): REQ-044 GET /v1/assets require_read + REQ-045 GET /v1/daemons caller-scope
REQ-044: list_catalog_assets 의존성을 require_admin → require_read로 완화. asset 쓰기(POST/PATCH/DELETE)는 require_admin 유지. REQ-045: GET /v1/daemons 신규 엔드포인트 — caller 소유 데몬만 반환(require_read). user_id=None(env 토큰)은 빈 목록 200. DaemonResponse/DaemonListResponse 스키마 추가. 기존 테스트(test_v1.py, test_asset_catalog.py, test_daemon_assignment.py, test_tags_master.py) REQ-044 권한 완화에 맞게 업데이트. test_v1_daemons.py 신규 추가(13 케이스). pytest 472 passed, 0 failed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VHtM5Ws1HKCKQ4JGi6GRb5
This commit is contained in:
+56
-16
@@ -610,18 +610,26 @@ def test_v1_assets_requires_service_token(monkeypatch) -> None:
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
def test_v1_assets_rejects_env_token() -> None:
|
||||
def test_v1_assets_env_read_token_returns_200(monkeypatch) -> None:
|
||||
"""REQ-044: env read token is now accepted (require_read) — list_catalog_assets."""
|
||||
from app.core.config import get_settings
|
||||
from app.domain.models import Asset
|
||||
import datetime as _dt
|
||||
settings = _make_settings(db_enabled=False, read_tokens_raw="env-token")
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.list_assets.return_value = []
|
||||
mock_repo.get_tags_by_ids.return_value = {}
|
||||
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
|
||||
with patch("app.api.v1_assets.get_portfolio_repository_from_settings", return_value=mock_repo):
|
||||
resp = client.get("/v1/assets", headers=_service_token_header("env-token"))
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["total"] == 0
|
||||
|
||||
|
||||
def test_v1_assets_returns_asset_list(monkeypatch) -> None:
|
||||
"""GET /v1/assets requires admin — service token is rejected."""
|
||||
"""REQ-044: GET /v1/assets now accepts read service token — returns 200 with catalog list."""
|
||||
monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "true")
|
||||
principal = TokenPrincipal(
|
||||
token_id="t1", user_id="user-1", permission="read",
|
||||
@@ -629,12 +637,16 @@ def test_v1_assets_returns_asset_list(monkeypatch) -> None:
|
||||
)
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.get_token_principal.return_value = principal
|
||||
mock_repo.list_assets.return_value = []
|
||||
mock_repo.get_tags_by_ids.return_value = {}
|
||||
|
||||
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo):
|
||||
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()
|
||||
resp = client.get("/v1/assets", headers=_service_token_header())
|
||||
|
||||
assert resp.status_code == 403
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["total"] == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -642,7 +654,7 @@ def test_v1_assets_returns_asset_list(monkeypatch) -> None:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_v1_assets_user_token(monkeypatch) -> None:
|
||||
"""User token is rejected — admin catalog endpoint requires admin permission."""
|
||||
"""REQ-044: user read token is now accepted (require_read) — returns 200."""
|
||||
settings = _make_app_db(monkeypatch)
|
||||
principal = TokenPrincipal(
|
||||
token_id="t1", user_id="user-1", permission="read",
|
||||
@@ -650,12 +662,16 @@ def test_v1_assets_user_token(monkeypatch) -> None:
|
||||
)
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.get_token_principal.return_value = principal
|
||||
mock_repo.list_assets.return_value = []
|
||||
mock_repo.get_tags_by_ids.return_value = {}
|
||||
|
||||
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo):
|
||||
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 == 403
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["total"] == 0
|
||||
|
||||
|
||||
def test_v1_assets_admin_token_default(monkeypatch) -> None:
|
||||
@@ -716,24 +732,44 @@ def test_v1_assets_admin_token_user_id_param(monkeypatch) -> None:
|
||||
|
||||
|
||||
def test_v1_assets_market_filter(monkeypatch) -> None:
|
||||
"""Service token is rejected by admin catalog endpoint — REQ-010 AC-12."""
|
||||
"""REQ-044: read service token with market filter now returns 200 (require_read)."""
|
||||
from app.domain.models import Asset
|
||||
import datetime as _dt
|
||||
settings = _make_app_db(monkeypatch)
|
||||
krx_asset = Asset(
|
||||
asset_id="a-krx", 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),
|
||||
)
|
||||
nasdaq_asset = Asset(
|
||||
asset_id="a-nasdaq", symbol="AAPL", markets=["NASDAQ"],
|
||||
display_name="Apple", 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="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_assets.return_value = [krx_asset, nasdaq_asset]
|
||||
mock_repo.get_tags_by_ids.return_value = {}
|
||||
|
||||
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo):
|
||||
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?market=KRX", headers=_service_token_header())
|
||||
|
||||
assert resp.status_code == 403
|
||||
assert resp.status_code == 200
|
||||
items = resp.json()["items"]
|
||||
assert len(items) == 1
|
||||
assert items[0]["symbol"] == "005930"
|
||||
|
||||
|
||||
def test_v1_assets_asset_id_is_uuid(monkeypatch) -> None:
|
||||
"""Service token is rejected by admin catalog endpoint."""
|
||||
def test_v1_assets_read_token_200(monkeypatch) -> None:
|
||||
"""REQ-044: read service token accepted by GET /v1/assets (require_read)."""
|
||||
settings = _make_app_db(monkeypatch)
|
||||
principal = TokenPrincipal(
|
||||
token_id="t1", user_id="user-1", permission="read",
|
||||
@@ -741,12 +777,16 @@ def test_v1_assets_asset_id_is_uuid(monkeypatch) -> None:
|
||||
)
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.get_token_principal.return_value = principal
|
||||
mock_repo.list_assets.return_value = []
|
||||
mock_repo.get_tags_by_ids.return_value = {}
|
||||
|
||||
with patch("app.core.auth.get_portfolio_repository_from_settings", return_value=mock_repo):
|
||||
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 == 403
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["total"] == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user