feat(bat-021): REQ-047 Step2 동반 코드 수정 + 실 DB 통합검증 + 런북

- ensure_schema() broker_credentials: daemon_id NOT NULL, PK=(daemon_id,broker)로 갱신
- _row_to_broker_credential: Step2 컬럼 순서(daemon_id 선두)로 갱신
- upsert_broker_credential: ON CONFLICT (daemon_id,broker)로 수정, fallback try/except 제거
- list_broker_credentials: fallback 제거, SELECT 컬럼 순서 갱신
- test_bat020_broker_credential_domain.py: AC-3/AC-4 행 순서를 Step2 포맷으로 갱신
- tests/test_bat021_step2_ensure_schema_and_upsert.py: 12개 신규 테스트
- docs/runbooks/bat-021-req047-step2-migration-runbook.md: 운영 적용 런북
- docs/batches/bat-021: batch 문서(index/planning/design/implementation/verification)

실 PostgreSQL 통합검증(AC-1~7 전건 PASS):
  Step1 upgrade → backfill → Step2 upgrade → downgrade 시나리오 포함
  NULL 사전검증(upgrade 거부) 및 downgrade 안전장치(중복 시 RuntimeError) 확인

521 passed, 0 failed (기존 509 + 신규 12)
This commit is contained in:
2026-06-28 10:19:27 +09:00
parent 7e3b34287f
commit 3e1b571906
10 changed files with 931 additions and 84 deletions
+17 -12
View File
@@ -42,37 +42,42 @@ def test_broker_credential_daemon_id_set() -> None:
# ---------------------------------------------------------------------------
# REQ-046 AC-3: _row_to_broker_credential 7컬럼(daemon_id 포함) 처리
# 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: 7컬럼 행(daemon_id 포함) 변환 시 daemon_id 세팅."""
"""REQ-046 AC-3 / BAT-021: Step2 컬럼 순서(daemon_id 선두) 행 변환 검증."""
from app.repositories.postgres import PostgresPortfolioRepository
now = datetime.now(timezone.utc)
row = ("user-1", "kis", "12345678", "encrypted", 2, now, "daemon-xyz")
# 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
assert cred.daemon_id == "daemon-xyz"
# ---------------------------------------------------------------------------
# REQ-046 AC-4: _row_to_broker_credential 6컬럼(daemon_id 없음) 하위호환
# 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: 6컬럼 행(daemon_id 없음) 변환 시 daemon_id=None (하위호환)."""
"""REQ-046 AC-4 / BAT-021: credential_version 없는 5컬럼 행 → credential_version 기본값 1."""
from app.repositories.postgres import PostgresPortfolioRepository
now = datetime.now(timezone.utc)
row = ("user-1", "kis", "12345678", "encrypted", 1, now)
# 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 is None
assert cred.daemon_id == "daemon-abc"
assert cred.credential_version == 1
assert cred.last_rotated_at is None
# ---------------------------------------------------------------------------
@@ -109,11 +114,11 @@ class _FakeConn:
def _make_rows_for_user(user_id: str):
"""daemon_id "d1", "d2" 각각 1건씩."""
"""Step2 컬럼 순서: (daemon_id, user_id, broker, account_ref, encrypted_payload, credential_version, last_rotated_at)."""
now = datetime.now(timezone.utc)
return [
(user_id, "kis", "ref-d1", "enc-d1", 1, now, "d1"),
(user_id, "kis2", "ref-d2", "enc-d2", 1, now, "d2"),
("d1", user_id, "kis", "ref-d1", "enc-d1", 1, now),
("d2", user_id, "kis2", "ref-d2", "enc-d2", 1, now),
]