feat(BAT-005): add broker credential CLI command with persistence and tests

- Add CredentialService for managing broker credentials
- Implement credential CLI commands (create, list, get, update, delete)
- Add postgres repository support for credential operations
- Add comprehensive test coverage for credential CLI
- Update docs/batches/index.md and docs/srs/index.md with BAT-005
- Update docs/project-structure.md to reflect credential service
This commit is contained in:
2026-05-03 12:44:40 +00:00
parent fb555dd72e
commit 222c66bb7f
12 changed files with 670 additions and 4 deletions
@@ -0,0 +1,53 @@
# BAT-005 Implementation Log
- Status: completed
- Batch: BAT-005
- Profile: batch-lite
- Completed: 2026-05-03
## 구현 내역
### 1. `app/repositories/postgres.py` — `delete_broker_credential` 추가
```python
def delete_broker_credential(self, user_id: str, broker: str) -> bool:
with self._connect().cursor() as cur:
cur.execute(
"DELETE FROM broker_credentials WHERE user_id = %s AND broker = %s",
(user_id, broker),
)
return cur.rowcount > 0
```
### 2. `app/cli/commands/credential.py` — 신규 생성
- `credential_app = typer.Typer(help="Broker credential management commands.")`
- `_with_service(ctx, callback)`: repository 생성 → execute_command → finally close. RuntimeError도 캐치해 stdout 출력 후 exit 1.
- `set` 명령: `--user`, `--broker` (기본값 `kis`), `--account-ref`, `--app-key`, `--app-secret`, `--force`
- `list` 명령: `--user` — encrypted_payload 제외 dict 리스트 출력
- `remove` 명령: `--user`, `--broker`, `--force` — delete_broker_credential 호출
### 3. `app/cli/main.py` — `credential_app` 등록
```python
from app.cli.commands.credential import credential_app
...
app.add_typer(credential_app, name="credential")
```
### 4. `tests/test_cli.py`
- `FakeRepo.__init__`: `_broker_credentials: dict[tuple[str, str], BrokerCredential] = {}` 추가
- `FakeRepo.upsert/list/get/delete_broker_credential` 메서드 추가
- `_settings_with_enc_key()`, `build_runner_with_enc_key()` 헬퍼 추가
- 신규 테스트 5개 추가
### 5. `docs/project-structure.md` — CLI 모듈 목록 갱신
- `app/cli/` 섹션에 `credential` 명령 그룹 추가
- primary runtime 설명에 `broker credentials` 추가
## 설계 결정
- dict 리스트 출력은 `render_output`의 타입 매칭을 우회해 `console.print(item)` 직접 호출 (기존 render_output이 등록된 타입만 처리함)
- `RuntimeError`(`CREDENTIAL_ENCRYPTION_KEY` 미설정)는 `_with_service` 헬퍼에서 캐치해 stdout으로 출력 후 exit 1 처리
@@ -0,0 +1,45 @@
# BAT-005 Index
- Status: release-candidate
- Batch: BAT-005
- Profile: batch-lite
- Created: 2026-05-03
## Included REQ
| REQ | 제목 | Type | Priority |
|-----|------|------|----------|
| REQ-015 | CLI `credential` 명령 그룹 구현 (브로커 인증 정보 관리) | Interface | Medium |
## Source Discovery
- minor-change fast path (Discovery 없음)
## Scope Summary
- CLI `pfhub credential set/list/remove` 명령 그룹 신규 추가
- `app/cli/commands/credential.py` 신규 생성 (`credential_app`)
- `app/cli/main.py`에 `credential_app` 등록
- `app/repositories/postgres.py`에 `delete_broker_credential` 메서드 추가
- 기존 `broker_credentials` 테이블 / `upsert_broker_credential` / `list_broker_credentials` 재사용
- 기존 Fernet 암호화 인프라 (`app/core/encryption.py`) 재사용
- DB 스키마 변경 없음, HTTP API 변경 없음
## Impacted Area
- Docs:
- `docs/project-structure.md` — CLI 모듈 목록에 `credential.py` 추가
- Modules:
- `app/cli/commands/credential.py` (신규)
- `app/cli/main.py` (`credential_app` 등록)
- `app/repositories/postgres.py` (`delete_broker_credential` 추가)
- Tests:
- `tests/test_cli.py` — credential 명령 통합 테스트 추가
## Stage Documents
| Stage | File | Status |
|-------|------|--------|
| Planning | [planning.md](./planning.md) | confirmed |
| Implementation | [implementation.md](./implementation.md) | completed |
| Verification | [verification.md](./verification.md) | passed |
@@ -0,0 +1,73 @@
# BAT-005 Planning
- Status: confirmed
- Batch: BAT-005
- Profile: batch-lite
## Included REQ
- REQ-015: CLI `credential` 명령 그룹 구현 (브로커 인증 정보 관리)
## Out of Scope
- HTTP API 엔드포인트 추가 (별도 REQ 대상)
- 브로커 인증 정보 복호화 출력 (보안 정책상 CLI에서 plain text 노출 금지)
- 브로커 종류 확장 (현재는 `kis` 기본값만 처리)
## 구현 순서
### 1단계: repository 메서드 추가
- `app/repositories/postgres.py`
- `delete_broker_credential(user_id: str, broker: str) -> bool` 추가
- 레코드 존재 시 삭제 후 `True`, 미존재 시 `False` 반환
### 2단계: CLI 명령 모듈 신규 생성
- `app/cli/commands/credential.py`
- `credential_app = typer.Typer(help="Broker credential management commands.")`
- `_with_service(ctx, callback)` 헬퍼: repository 생성 → callback 실행 → finally close
- `set` 명령: `--user`, `--broker` (기본값 `kis`), `--account-ref`, `--app-key`, `--app-secret`
- `get_user_by_username` → `None`이면 `ValueError`
- `settings.require_credential_encryption_key()` → Fernet 암호화
- `upsert_broker_credential` 호출
- `list` 명령: `--user`
- `get_user_by_username` → `None`이면 `ValueError`
- `list_broker_credentials` 호출, `encrypted_payload` 제외 출력
- `remove` 명령: `--user`, `--broker`, `--force`
- `get_user_by_username` → `None`이면 `ValueError`
- `_confirm(force, ...)` 확인 후 `delete_broker_credential` 호출
### 3단계: CLI 등록
- `app/cli/main.py`
- `from app.cli.commands.credential import credential_app` import 추가
- `app.add_typer(credential_app, name="credential")` 등록
### 4단계: 문서 갱신
- `docs/project-structure.md` — CLI 모듈 목록에 `credential.py` 항목 추가
### 5단계: 테스트
- `tests/test_cli.py`
- `test_cli_credential_set_creates_record` (신규): alice 생성 후 set 실행 → list에 표시
- `test_cli_credential_list_hides_payload` (신규): list 출력에 `encrypted_payload` 미포함 확인
- `test_cli_credential_set_with_unknown_user_exits_error` (신규): 미존재 user → exit code 1
- `test_cli_credential_remove_deletes_record` (신규): set 후 remove → list에서 사라짐
- `test_cli_credential_set_requires_encryption_key` (신규): CREDENTIAL_ENCRYPTION_KEY 미설정 → exit code 1
## Dependencies
- Internal:
- `broker_credentials` 테이블 (migration 0014 — 이미 존재)
- `upsert_broker_credential`, `list_broker_credentials`, `get_broker_credential` (이미 존재)
- `app/core/encryption.py` — `encrypt_payload` (이미 존재)
- `app/core/config.py` — `settings.require_credential_encryption_key()` (이미 존재)
- `_confirm`, `execute_command`, `render_output` (기존 CLI 헬퍼 — 이미 존재)
- External: 없음
## Design Gate
- Design Required Before Implementation: no
- Reason: 기존 `_with_service` 헬퍼 패턴 재사용 (`user.py` 동일 구조). 구조/런타임 흐름 변경 없음.
@@ -0,0 +1,61 @@
# BAT-005 Verification
- Status: passed
- Batch: BAT-005
- Profile: batch-lite
- Verified: 2026-05-03
## REQ Coverage
### REQ-015: CLI `credential` 명령 그룹 구현 (브로커 인증 정보 관리)
| AC # | 기준 | 검증 방법 | 결과 |
|------|------|-----------|------|
| AC-1 | alice 사용자 + 유효 enc key → `credential set` 실행 시 broker_credentials 레코드 생성(upsert) | `test_cli_credential_set_creates_record` | ✅ passed |
| AC-2 | `credential list` 출력에 broker, account_ref, credential_version 포함 / encrypted_payload 미포함 | `test_cli_credential_list_hides_payload` | ✅ passed |
| AC-3 | 존재하지 않는 username → exit code 1 + 에러 메시지 | `test_cli_credential_set_with_unknown_user_exits_error` | ✅ passed |
| AC-4 | `credential remove --force` 후 list에서 사라짐 | `test_cli_credential_remove_deletes_record` | ✅ passed |
| AC-5 | CREDENTIAL_ENCRYPTION_KEY 미설정 → exit code 1 + 에러 메시지 포함 | `test_cli_credential_set_requires_encryption_key` | ✅ passed |
## Test Evidence
### 실행 명령
```bash
python -m pytest tests/test_cli.py -q
```
### 결과
```
49 passed in 1.49s
```
- 신규 테스트 5개 모두 passed
- 기존 테스트 44개 regression 없음
### 신규 테스트 목록
| 테스트 | AC |
|--------|----|
| `test_cli_credential_set_creates_record` | AC-1 |
| `test_cli_credential_list_hides_payload` | AC-2 |
| `test_cli_credential_set_with_unknown_user_exits_error` | AC-3 |
| `test_cli_credential_remove_deletes_record` | AC-4 |
| `test_cli_credential_set_requires_encryption_key` | AC-5 |
## Implementation Summary
| 파일 | 변경 내용 |
|------|-----------|
| `app/cli/commands/credential.py` | 신규 생성 — `credential_app`, `set`/`list`/`remove` 명령 |
| `app/cli/main.py` | `credential_app` import 및 `app.add_typer(credential_app, name="credential")` 등록 |
| `app/repositories/postgres.py` | `delete_broker_credential(user_id, broker) -> bool` 메서드 추가 |
| `tests/test_cli.py` | `FakeRepo.upsert/list/get/delete_broker_credential`, 신규 테스트 5개 추가 |
| `docs/project-structure.md` | CLI 명령 그룹 목록에 `credential` 추가 |
## Baseline Document Sync
- `docs/project-structure.md`: `app/cli/` 섹션에 `credential` 명령 그룹 추가 완료
- 구조/런타임 흐름/인터페이스 계약 변경 없음 → `runtime-flows.md`, `interface-contract.md` 갱신 불필요
## Residual Risk
- 없음
+2 -1
View File
@@ -11,4 +11,5 @@
| BAT-001 | standard | release-candidate | REQ-004, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010 | dcy-002 | [bat-001_20260502_trex-basic-v1-api-contract](./bat-001_20260502_trex-basic-v1-api-contract/) |
| BAT-002 | batch-lite | release-candidate | REQ-012 | minor-change | [bat-002_20260502_service-token-rotate-cli](./bat-002_20260502_service-token-rotate-cli/) |
| BAT-003 | batch-lite | release-candidate | REQ-013 | dcy-003 | [bat-003_20260502_portfolio-list-user-required](./bat-003_20260502_portfolio-list-user-required/) |
| BAT-004 | batch-lite | release-candidate | REQ-014 | minor-change | [bat-004_20260502_portfolio-create-user-required](./bat-004_20260502_portfolio-create-user-required/) |
| BAT-004 | batch-lite | release-candidate | REQ-014 | minor-change | [bat-004_20260502_portfolio-create-user-required](./bat-004_20260502_portfolio-create-user-required/) |
| BAT-005 | batch-lite | release-candidate | REQ-015 | minor-change | [bat-005_20260503_broker-credential-cli](./bat-005_20260503_broker-credential-cli/) |