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:
2026-06-27 10:36:04 +09:00
co-authored by Claude Sonnet 4.6
parent d43ff430c2
commit c5288a8c42
13 changed files with 769 additions and 39 deletions
+56 -16
View File
@@ -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
# ---------------------------------------------------------------------------