"""BAT-021: REQ-047 Step2 동반 코드 수정 단위 테스트. 검증 범위: - ensure_schema() broker_credentials CREATE TABLE: daemon-scoped PK 정의 - _row_to_broker_credential: Step2 컬럼 순서 (daemon_id 선두) - upsert_broker_credential: ON CONFLICT (daemon_id, broker), fallback 제거 - list_broker_credentials: fallback 제거, Step2 컬럼 순서 """ from __future__ import annotations from datetime import datetime, timezone from unittest.mock import MagicMock, patch import pytest from app.domain.models import BrokerCredential from app.repositories.postgres import PortfolioDBConfig, PostgresPortfolioRepository # --------------------------------------------------------------------------- # 공통 픽스처 # --------------------------------------------------------------------------- def _make_config() -> PortfolioDBConfig: return PortfolioDBConfig( host="localhost", port=5432, dbname="test", user="test", password="test", sslmode="disable", ) def _make_repo() -> PostgresPortfolioRepository: return PostgresPortfolioRepository(_make_config()) # --------------------------------------------------------------------------- # ensure_schema broker_credentials 스키마 정의 검증 # --------------------------------------------------------------------------- def test_ensure_schema_broker_credentials_ddl_contains_daemon_id_pk() -> None: """BAT-021: ensure_schema()의 broker_credentials CREATE TABLE에 daemon_id NOT NULL과 PRIMARY KEY (daemon_id, broker)가 포함되어 있다.""" import inspect import app.repositories.postgres as repo_module source = inspect.getsource(repo_module) # CREATE TABLE에 daemon_id NOT NULL 포함 확인 assert "daemon_id TEXT NOT NULL" in source, "ensure_schema broker_credentials에 daemon_id NOT NULL 없음" # PRIMARY KEY (daemon_id, broker) 포함 확인 assert "PRIMARY KEY (daemon_id, broker)" in source, "ensure_schema broker_credentials PK가 (daemon_id,broker) 아님" def test_ensure_schema_broker_credentials_no_old_pk_user_id_broker() -> None: """BAT-021: ensure_schema()의 broker_credentials CREATE TABLE에 구 PK (user_id, broker)가 없다.""" import inspect import app.repositories.postgres as repo_module source = inspect.getsource(repo_module) # ensure_schema 함수 영역만 검사: broker_credentials CREATE TABLE # "PRIMARY KEY (user_id, broker)" 패턴이 없어야 함 assert "PRIMARY KEY (user_id, broker)" not in source, "ensure_schema에 구 PK (user_id,broker) 잔존" # --------------------------------------------------------------------------- # _row_to_broker_credential Step2 컬럼 순서 # --------------------------------------------------------------------------- def test_row_to_broker_credential_step2_7col_canonical_order() -> None: """BAT-021: Step2 표준 7컬럼 행 변환 — daemon_id 선두.""" now = datetime.now(timezone.utc) # Step2: (daemon_id, user_id, broker, account_ref, encrypted_payload, credential_version, last_rotated_at) row = ("daemon-abc", "user-1", "kis", "ACC-001", "enc-payload", 3, now) cred = PostgresPortfolioRepository._row_to_broker_credential(row) assert cred.daemon_id == "daemon-abc" assert cred.user_id == "user-1" assert cred.broker == "kis" assert cred.account_ref == "ACC-001" assert cred.encrypted_payload == "enc-payload" assert cred.credential_version == 3 assert cred.last_rotated_at == now def test_row_to_broker_credential_step2_5col_no_version() -> None: """BAT-021: 5컬럼 행(credential_version 없음) — 기본값 1, last_rotated_at=None.""" row = ("daemon-xyz", "user-2", "kis2", "ACC-002", "enc-2") cred = PostgresPortfolioRepository._row_to_broker_credential(row) assert cred.daemon_id == "daemon-xyz" assert cred.user_id == "user-2" assert cred.broker == "kis2" assert cred.credential_version == 1 assert cred.last_rotated_at is None # --------------------------------------------------------------------------- # upsert_broker_credential: ON CONFLICT (daemon_id, broker) # --------------------------------------------------------------------------- def test_upsert_broker_credential_uses_daemon_id_conflict_target() -> None: """BAT-021: upsert_broker_credential SQL에 ON CONFLICT (daemon_id, broker) 포함 확인.""" import inspect import app.repositories.postgres as repo_module # upsert_broker_credential 메서드 소스 추출 source = inspect.getsource( PostgresPortfolioRepository.upsert_broker_credential ) assert "ON CONFLICT (daemon_id, broker)" in source, \ "upsert_broker_credential에 ON CONFLICT (daemon_id, broker) 없음" def test_upsert_broker_credential_no_old_conflict_target() -> None: """BAT-021: upsert_broker_credential SQL에 구 ON CONFLICT (user_id, broker) 없음.""" import inspect source = inspect.getsource( PostgresPortfolioRepository.upsert_broker_credential ) assert "ON CONFLICT (user_id, broker)" not in source, \ "upsert_broker_credential에 구 ON CONFLICT (user_id,broker) 잔존" def test_upsert_broker_credential_no_fallback_try_except() -> None: """BAT-021: upsert_broker_credential에 fallback try/except 없음 (트랜잭션 abort 이슈 해소).""" import inspect source = inspect.getsource( PostgresPortfolioRepository.upsert_broker_credential ) # Fallback try/except는 "Fallback" 주석과 함께 있었음 — 완전 제거 확인 assert "Fallback: daemon_id" not in source, \ "upsert_broker_credential에 Fallback try/except 잔존" def test_upsert_broker_credential_mock_returns_step2_row() -> None: """BAT-021: upsert_broker_credential mock 기반 — Step2 RETURNING 순서(daemon_id 선두)로 BrokerCredential 생성.""" now = datetime.now(timezone.utc) # Step2 RETURNING 순서: daemon_id, user_id, broker, account_ref, encrypted_payload, credential_version, last_rotated_at mock_row = ("daemon-test", "user-test", "kis", "REF-001", "enc-test", 1, now) repo = _make_repo() mock_cursor = MagicMock() mock_cursor.__enter__ = lambda s: mock_cursor mock_cursor.__exit__ = MagicMock(return_value=False) # user existence check mock_cursor.fetchone.side_effect = [MagicMock(), mock_row] # user exists, then credential row mock_conn = MagicMock() mock_conn.cursor.return_value = mock_cursor repo._connection = mock_conn with patch.object(repo, "ensure_schema"): cred = repo.upsert_broker_credential( "user-test", "kis", account_ref="REF-001", encrypted_payload="enc-test", daemon_id="daemon-test", ) assert cred.daemon_id == "daemon-test" assert cred.user_id == "user-test" assert cred.broker == "kis" assert cred.account_ref == "REF-001" # --------------------------------------------------------------------------- # list_broker_credentials: fallback 제거, Step2 컬럼 순서 # --------------------------------------------------------------------------- def test_list_broker_credentials_no_fallback_try_except() -> None: """BAT-021: list_broker_credentials에 fallback try/except 없음.""" import inspect source = inspect.getsource( PostgresPortfolioRepository.list_broker_credentials ) assert "Fallback: daemon_id" not in source, \ "list_broker_credentials에 Fallback try/except 잔존" def test_list_broker_credentials_step2_select_order() -> None: """BAT-021: list_broker_credentials SELECT 쿼리에 daemon_id 선두 컬럼 순서.""" import inspect source = inspect.getsource( PostgresPortfolioRepository.list_broker_credentials ) # daemon_id가 SELECT에서 user_id보다 앞에 있어야 함 daemon_pos = source.find("SELECT daemon_id") assert daemon_pos != -1, "list_broker_credentials SELECT에 daemon_id 선두 없음" def test_list_broker_credentials_mock_returns_step2_rows() -> None: """BAT-021: list_broker_credentials mock — Step2 컬럼 순서로 BrokerCredential 생성.""" now = datetime.now(timezone.utc) # Step2 컬럼 순서: (daemon_id, user_id, broker, account_ref, encrypted_payload, credential_version, last_rotated_at) mock_rows = [ ("daemon-1", "user-1", "kis", "REF-A", "enc-a", 1, now), ("daemon-2", "user-1", "kis2", "REF-B", "enc-b", 1, now), ] repo = _make_repo() mock_cursor = MagicMock() mock_cursor.__enter__ = lambda s: mock_cursor mock_cursor.__exit__ = MagicMock(return_value=False) mock_cursor.fetchall.return_value = mock_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 assert results[0].daemon_id == "daemon-1" assert results[0].broker == "kis" assert results[1].daemon_id == "daemon-2" assert results[1].broker == "kis2" def test_list_broker_credentials_daemon_id_filter_step2() -> None: """BAT-021: list_broker_credentials daemon_id 필터 — Step2 컬럼 순서.""" now = datetime.now(timezone.utc) mock_rows = [ ("daemon-1", "user-1", "kis", "REF-A", "enc-a", 1, now), ] repo = _make_repo() mock_cursor = MagicMock() mock_cursor.__enter__ = lambda s: mock_cursor mock_cursor.__exit__ = MagicMock(return_value=False) mock_cursor.fetchall.return_value = mock_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", daemon_id="daemon-1") assert len(results) == 1 assert results[0].daemon_id == "daemon-1" assert results[0].broker == "kis"