- 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)
165 lines
4.0 KiB
Markdown
165 lines
4.0 KiB
Markdown
# BAT-021 Verification
|
|
|
|
## pytest 실행 결과
|
|
|
|
```
|
|
python3 -m pytest tests/ --tb=short
|
|
521 passed, 11 warnings in 26.67s
|
|
```
|
|
|
|
기존 테스트: 509 passed (BAT-020 기준)
|
|
신규 테스트: 12 added (test_bat021_step2_ensure_schema_and_upsert.py)
|
|
기존 테스트 회귀: 0건
|
|
|
|
---
|
|
|
|
## REQ-047 AC 검증 (실 PostgreSQL)
|
|
|
|
### 검증 환경
|
|
|
|
- Docker: `portfolio-hub-timescaledb` (timescale/timescaledb:latest-pg16)
|
|
- DB: `portfolio_hub` (127.0.0.1:5432)
|
|
- 시작 alembic 상태: 20260601_0024 (ensure_schema()로 스키마 구성, alembic 미동기 상태)
|
|
- stamp 조치: `alembic stamp 20260605_0025` (DB 실제 스키마 = 0025 적용 상태 반영)
|
|
|
|
### AC-1: Step1 후 daemon_id TEXT NULL 컬럼 존재, PK (user_id,broker) 유지
|
|
|
|
```
|
|
alembic upgrade 20260628_0026
|
|
```
|
|
|
|
결과:
|
|
```
|
|
daemon_id | text | nullable=YES
|
|
PK: broker_credentials_pkey PRIMARY KEY btree (user_id, broker)
|
|
```
|
|
|
|
**PASS**
|
|
|
|
### AC-2: Step1 후 기존 데이터 daemon_id=NULL 보존
|
|
|
|
```sql
|
|
INSERT INTO broker_credentials ... VALUES ('test-user-1', 'kis', ...)
|
|
SELECT daemon_id FROM broker_credentials WHERE user_id='test-user-1';
|
|
-- 결과: NULL
|
|
```
|
|
|
|
**PASS**
|
|
|
|
### AC-3: Step2 후 PK (daemon_id, broker) 전환, daemon_id NOT NULL
|
|
|
|
```
|
|
# backfill: UPDATE broker_credentials SET daemon_id='test-daemon-1' WHERE daemon_id IS NULL
|
|
alembic upgrade 20260628_0027
|
|
```
|
|
|
|
결과:
|
|
```
|
|
daemon_id | text | nullable=NO
|
|
PK: broker_credentials_pkey PRIMARY KEY btree (daemon_id, broker)
|
|
```
|
|
|
|
**PASS**
|
|
|
|
### AC-4: Step2 후 user_id 컬럼 유지
|
|
|
|
```sql
|
|
SELECT column_name FROM information_schema.columns
|
|
WHERE table_name='broker_credentials' AND column_name='user_id';
|
|
-- 결과: user_id 존재
|
|
```
|
|
|
|
**PASS**
|
|
|
|
### AC-5: Step2 후 ensure_schema() 정합
|
|
|
|
```python
|
|
repo.ensure_schema() # 오류 없음
|
|
# DB 조회 결과:
|
|
# daemon_id: NOT NULL, PK=(daemon_id, broker), user_id 존재
|
|
```
|
|
|
|
**PASS**
|
|
|
|
### AC-6: Step1 downgrade — daemon_id 컬럼 제거, PK (user_id,broker) 복원
|
|
|
|
```
|
|
alembic downgrade 20260605_0025
|
|
```
|
|
|
|
결과:
|
|
```
|
|
broker_credentials: daemon_id 컬럼 없음
|
|
PK: broker_credentials_pkey PRIMARY KEY btree (user_id, broker)
|
|
```
|
|
|
|
**PASS**
|
|
|
|
### AC-7: Step2 downgrade 안전장치 — 중복 (user_id,broker) 존재 시 RuntimeError
|
|
|
|
```
|
|
# 2개 daemon이 같은 user/broker 사용하도록 데이터 구성 후 downgrade 시도
|
|
alembic downgrade 20260628_0026
|
|
```
|
|
|
|
결과:
|
|
```
|
|
RuntimeError: downgrade 불가: 1개 (user_id, broker) 조합이 중복됩니다.
|
|
같은 user의 여러 daemon이 동일 broker를 사용하고 있어
|
|
(user_id, broker) PK로 복원 시 데이터 손실이 발생합니다.
|
|
수동으로 데이터를 정리한 후 downgrade하세요.
|
|
```
|
|
|
|
**PASS**
|
|
|
|
---
|
|
|
|
## 동반 코드 검증
|
|
|
|
### upsert_broker_credential (ON CONFLICT + row mapper)
|
|
|
|
```python
|
|
cred = repo.upsert_broker_credential(
|
|
'bat021-test-user', 'kis',
|
|
account_ref='BAT021-ACCOUNT',
|
|
encrypted_payload='bat021-encrypted-payload',
|
|
daemon_id='bat021-daemon-1'
|
|
)
|
|
# 결과: daemon_id='bat021-daemon-1', user_id='bat021-test-user', broker='kis', account_ref='BAT021-ACCOUNT'
|
|
cred2 = repo.upsert_broker_credential(..., account_ref='BAT021-ACCOUNT-UPDATED')
|
|
# 결과: version=2, account_ref='BAT021-ACCOUNT-UPDATED' (idempotent update OK)
|
|
```
|
|
|
|
**PASS**
|
|
|
|
### list_broker_credentials (Step2 컬럼 순서)
|
|
|
|
```python
|
|
creds = repo.list_broker_credentials('bat021-test-user', daemon_id='bat021-daemon-1')
|
|
# 결과: 1건, daemon_id='bat021-daemon-1', broker='kis'
|
|
```
|
|
|
|
**PASS**
|
|
|
|
---
|
|
|
|
## Step2 NULL 사전 검증 (REQ-048 미수행 시 거부)
|
|
|
|
```
|
|
# daemon_id IS NULL 행 1건 존재 상태에서 Step2 upgrade 시도
|
|
RuntimeError: broker_credentials에 daemon_id IS NULL인 행이 1건 있습니다.
|
|
REQ-048 backfill-daemon-id CLI로 모든 행을 백필한 후 다시 실행하세요.
|
|
```
|
|
|
|
**PASS**
|
|
|
|
---
|
|
|
|
## 미해결/위험 사항
|
|
|
|
없음 — 이 batch의 TODO 2건 모두 해소됨:
|
|
1. ensure_schema() broker_credentials 갱신: **완료**
|
|
2. upsert ON CONFLICT (user_id,broker) → (daemon_id,broker) + fallback 제거: **완료**
|
|
|
|
실 운영 DB 적용은 런북에 따라 사장님 결재 후 운영자가 수행한다.
|