"""BAT-020: REQ-046 BrokerCredential daemon_id 필드 단위 테스트.""" from __future__ import annotations from datetime import datetime, timezone import pytest from app.domain.models import BrokerCredential # --------------------------------------------------------------------------- # REQ-046 AC-1: daemon_id=None 기본값 # --------------------------------------------------------------------------- def test_broker_credential_daemon_id_defaults_to_none() -> None: """REQ-046 AC-1: daemon_id 미지정 시 None이 기본값.""" cred = BrokerCredential( user_id="user-1", broker="kis", account_ref="12345678", encrypted_payload="encrypted", ) assert hasattr(cred, "daemon_id") assert cred.daemon_id is None # --------------------------------------------------------------------------- # REQ-046 AC-2: daemon_id 값 지정 # --------------------------------------------------------------------------- def test_broker_credential_daemon_id_set() -> None: """REQ-046 AC-2: daemon_id="daemon-abc" 지정 시 해당 값으로 세팅.""" cred = BrokerCredential( user_id="user-1", broker="kis", account_ref="12345678", encrypted_payload="encrypted", daemon_id="daemon-abc", ) assert cred.daemon_id == "daemon-abc" # --------------------------------------------------------------------------- # REQ-046 AC-3 / BAT-021: _row_to_broker_credential Step2 컬럼 순서 검증 # Step2(REQ-047) 이후 컬럼 순서: (daemon_id, user_id, broker, account_ref, encrypted_payload, credential_version, last_rotated_at) # --------------------------------------------------------------------------- def test_row_to_broker_credential_with_daemon_id() -> None: """REQ-046 AC-3 / BAT-021: Step2 컬럼 순서(daemon_id 선두) 행 변환 검증.""" from app.repositories.postgres import PostgresPortfolioRepository now = datetime.now(timezone.utc) # Step2 이후 RETURNING/SELECT 순서: daemon_id, user_id, broker, account_ref, encrypted_payload, credential_version, last_rotated_at row = ("daemon-xyz", "user-1", "kis", "12345678", "encrypted", 2, now) cred = PostgresPortfolioRepository._row_to_broker_credential(row) assert cred.daemon_id == "daemon-xyz" assert cred.user_id == "user-1" assert cred.broker == "kis" assert cred.account_ref == "12345678" assert cred.encrypted_payload == "encrypted" assert cred.credential_version == 2 assert cred.last_rotated_at == now # --------------------------------------------------------------------------- # REQ-046 AC-4 / BAT-021: _row_to_broker_credential credential_version 기본값 # Step2 이후 daemon_id는 항상 존재. 6컬럼(credential_version 없음) 기본값 1 확인. # --------------------------------------------------------------------------- def test_row_to_broker_credential_without_daemon_id() -> None: """REQ-046 AC-4 / BAT-021: credential_version 없는 5컬럼 행 → credential_version 기본값 1.""" from app.repositories.postgres import PostgresPortfolioRepository # Step2 이후 컬럼 순서: daemon_id, user_id, broker, account_ref, encrypted_payload (5컬럼) row = ("daemon-abc", "user-1", "kis", "12345678", "encrypted") cred = PostgresPortfolioRepository._row_to_broker_credential(row) assert cred.daemon_id == "daemon-abc" assert cred.credential_version == 1 assert cred.last_rotated_at is None # --------------------------------------------------------------------------- # REQ-046 AC-5/6: list_broker_credentials daemon_id 필터 (mock 기반) # --------------------------------------------------------------------------- class _FakeCursor: """psycopg Cursor 최소 mock.""" def __init__(self, rows): self._rows = rows self.last_query = None self.last_params = None def execute(self, query, params=None): self.last_query = query self.last_params = params def fetchall(self): return self._rows def __enter__(self): return self def __exit__(self, *args): pass class _FakeConn: def __init__(self, rows): self._rows = rows def cursor(self): return _FakeCursor(self._rows) def _make_rows_for_user(user_id: str): """Step2 컬럼 순서: (daemon_id, user_id, broker, account_ref, encrypted_payload, credential_version, last_rotated_at).""" now = datetime.now(timezone.utc) return [ ("d1", user_id, "kis", "ref-d1", "enc-d1", 1, now), ("d2", user_id, "kis2", "ref-d2", "enc-d2", 1, now), ] def test_list_broker_credentials_daemon_id_filter() -> None: """REQ-046 AC-5: daemon_id 필터 시 해당 daemon_id 행만 반환.""" from unittest.mock import MagicMock, patch from app.repositories.postgres import PostgresPortfolioRepository, PortfolioDBConfig rows = _make_rows_for_user("user-1") # daemon_id="d1" 필터 시 d1 행만 반환되는 mock d1_rows = [rows[0]] config = PortfolioDBConfig( host="localhost", port=5432, dbname="test", user="test", password="test", sslmode="disable", ) repo = PostgresPortfolioRepository(config) mock_cursor = MagicMock() mock_cursor.__enter__ = lambda s: mock_cursor mock_cursor.__exit__ = MagicMock(return_value=False) mock_cursor.fetchall.return_value = d1_rows mock_conn = MagicMock() mock_conn.cursor.return_value = mock_cursor repo._connection = mock_conn # ensure_schema 무력화 with patch.object(repo, "ensure_schema"): results = repo.list_broker_credentials("user-1", daemon_id="d1") assert len(results) == 1 assert results[0].daemon_id == "d1" assert results[0].broker == "kis" def test_list_broker_credentials_no_daemon_id_filter_returns_all() -> None: """REQ-046 AC-6: daemon_id 미지정 시 전체 반환 (하위호환).""" from unittest.mock import MagicMock, patch from app.repositories.postgres import PostgresPortfolioRepository, PortfolioDBConfig rows = _make_rows_for_user("user-1") config = PortfolioDBConfig( host="localhost", port=5432, dbname="test", user="test", password="test", sslmode="disable", ) repo = PostgresPortfolioRepository(config) mock_cursor = MagicMock() mock_cursor.__enter__ = lambda s: mock_cursor mock_cursor.__exit__ = MagicMock(return_value=False) mock_cursor.fetchall.return_value = rows mock_conn = MagicMock() mock_conn.cursor.return_value = mock_cursor repo._connection = mock_conn with patch.object(repo, "ensure_schema"): results = repo.list_broker_credentials("user-1") assert len(results) == 2