From 2470c0a00ce2cc800c98a4436d8c1287bfdf2791 Mon Sep 17 00:00:00 2001 From: Justin Ko Date: Fri, 5 Jun 2026 13:57:46 +0900 Subject: [PATCH] feat: apply rel-001 feedback cleanup --- app/api/health.py | 2 + app/api/v1_portfolios.py | 4 +- app/domain/models.py | 2 +- app/repositories/portfolio_repository.py | 2 +- app/repositories/postgres.py | 16 +- app/schemas/auth.py | 4 +- app/schemas/v1.py | 2 +- .../design.md | 32 +++ .../implementation.md | 53 +++++ .../index.md | 34 +++ .../planning.md | 50 ++++ .../verification.md | 60 +++++ docs/batches/index.md | 1 + docs/data-model.md | 14 +- ...y-005_20260601_rel-001-feedback-cleanup.md | 224 ++++++++++++++++++ docs/discovery/index.md | 1 + docs/interface-contract.md | 5 +- docs/runtime-flows.md | 1 + ..._login-request-extra-forbid-enforcement.md | 126 ++++++++++ ...eq-035_health-response-db-enabled-field.md | 128 ++++++++++ ...-trades-asset-id-to-portfolio-id-rename.md | 132 +++++++++++ docs/srs/index.md | 11 +- ..._0024_ledger_trades_portfolio_id_rename.py | 53 +++++ tests/test_app.py | 25 ++ tests/test_auth.py | 32 +++ tests/test_daemon_assignment.py | 2 +- tests/test_v1.py | 4 +- tests/test_v1_portfolios_ledger.py | 4 +- 28 files changed, 992 insertions(+), 32 deletions(-) create mode 100644 docs/batches/bat-015_20260601_rel-001-feedback-cleanup/design.md create mode 100644 docs/batches/bat-015_20260601_rel-001-feedback-cleanup/implementation.md create mode 100644 docs/batches/bat-015_20260601_rel-001-feedback-cleanup/index.md create mode 100644 docs/batches/bat-015_20260601_rel-001-feedback-cleanup/planning.md create mode 100644 docs/batches/bat-015_20260601_rel-001-feedback-cleanup/verification.md create mode 100644 docs/discovery/dcy-005_20260601_rel-001-feedback-cleanup.md create mode 100644 docs/srs/Interface/req-034_login-request-extra-forbid-enforcement.md create mode 100644 docs/srs/Interface/req-035_health-response-db-enabled-field.md create mode 100644 docs/srs/Migration/req-033_ledger-trades-asset-id-to-portfolio-id-rename.md create mode 100644 migrations/versions/20260601_0024_ledger_trades_portfolio_id_rename.py diff --git a/app/api/health.py b/app/api/health.py index 0b4564f..d7da7e3 100644 --- a/app/api/health.py +++ b/app/api/health.py @@ -19,6 +19,7 @@ class HealthResponse(BaseModel): service: str version: str checked_at: str + db_enabled: bool @router.get("/health", response_model=HealthResponse) @@ -29,4 +30,5 @@ def healthcheck() -> HealthResponse: service="portfolio-hub", version=settings.app_version, checked_at=datetime.now(_KST).isoformat(), + db_enabled=settings.db_enabled, ) diff --git a/app/api/v1_portfolios.py b/app/api/v1_portfolios.py index 078a3c2..bdf13e2 100644 --- a/app/api/v1_portfolios.py +++ b/app/api/v1_portfolios.py @@ -545,7 +545,7 @@ def record_ledger_trade_v1( ) trade, created = repo.insert_ledger_trade( - asset_id=portfolio_id, + portfolio_id=portfolio_id, order_id=body.order_id, execution_id=body.execution_id, side=body.side, @@ -573,7 +573,7 @@ def record_ledger_trade_v1( return LedgerTradeResponse( ledger_entry_id=trade.ledger_entry_id, - asset_id=trade.asset_id, + portfolio_id=trade.portfolio_id, order_id=trade.order_id, execution_id=trade.execution_id, side=trade.side, diff --git a/app/domain/models.py b/app/domain/models.py index 5b87f74..b1e6250 100644 --- a/app/domain/models.py +++ b/app/domain/models.py @@ -174,7 +174,7 @@ class BrokerCredential: @dataclass class LedgerTrade: ledger_entry_id: int - asset_id: str + portfolio_id: str order_id: str execution_id: str side: str diff --git a/app/repositories/portfolio_repository.py b/app/repositories/portfolio_repository.py index 33b8050..80b1a77 100644 --- a/app/repositories/portfolio_repository.py +++ b/app/repositories/portfolio_repository.py @@ -253,7 +253,7 @@ class PortfolioRepository(Protocol): def insert_ledger_trade( self, *, - asset_id: str, + portfolio_id: str, order_id: str, execution_id: str, side: str, diff --git a/app/repositories/postgres.py b/app/repositories/postgres.py index cdb3946..9ca9af6 100644 --- a/app/repositories/postgres.py +++ b/app/repositories/postgres.py @@ -1267,7 +1267,7 @@ class PostgresPortfolioRepository: def insert_ledger_trade( self, *, - asset_id: str, + portfolio_id: str, order_id: str, execution_id: str, side: str, @@ -1282,17 +1282,17 @@ class PostgresPortfolioRepository: cur.execute( """ INSERT INTO ledger_trades - (asset_id, order_id, execution_id, side, quantity, price, + (portfolio_id, order_id, execution_id, side, quantity, price, fee_amount, tax_amount, executed_at) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT ON CONSTRAINT uq_ledger_trades_idempotency DO NOTHING RETURNING - ledger_entry_id, asset_id, order_id, execution_id, side, + ledger_entry_id, portfolio_id, order_id, execution_id, side, quantity::float8, price::float8, fee_amount::float8, tax_amount::float8, executed_at, recorded_at """, - (asset_id, order_id, execution_id, side, quantity, price, + (portfolio_id, order_id, execution_id, side, quantity, price, fee_amount, tax_amount, executed_at), ) row = cur.fetchone() @@ -1302,14 +1302,14 @@ class PostgresPortfolioRepository: cur.execute( """ SELECT - ledger_entry_id, asset_id, order_id, execution_id, side, + ledger_entry_id, portfolio_id, order_id, execution_id, side, quantity::float8, price::float8, fee_amount::float8, tax_amount::float8, executed_at, recorded_at FROM ledger_trades - WHERE asset_id = %s AND order_id = %s AND execution_id = %s + WHERE portfolio_id = %s AND order_id = %s AND execution_id = %s """, - (asset_id, order_id, execution_id), + (portfolio_id, order_id, execution_id), ) row = cur.fetchone() return self._row_to_ledger_trade(row), False @@ -1319,7 +1319,7 @@ class PostgresPortfolioRepository: from app.domain.models import LedgerTrade return LedgerTrade( ledger_entry_id=row[0], - asset_id=row[1], + portfolio_id=row[1], order_id=row[2], execution_id=row[3], side=row[4], diff --git a/app/schemas/auth.py b/app/schemas/auth.py index 2ba84b7..b4952fb 100644 --- a/app/schemas/auth.py +++ b/app/schemas/auth.py @@ -5,12 +5,14 @@ from __future__ import annotations from datetime import datetime from typing import Literal -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator from app.schemas.user import UserResponse class LoginRequest(BaseModel): + model_config = ConfigDict(extra="forbid") + username: str = Field(min_length=1) password: str = Field(min_length=1) diff --git a/app/schemas/v1.py b/app/schemas/v1.py index 64d4442..39b01c5 100644 --- a/app/schemas/v1.py +++ b/app/schemas/v1.py @@ -121,7 +121,7 @@ class LedgerTradeRequest(BaseModel): class LedgerTradeResponse(BaseModel): ledger_entry_id: int - asset_id: str + portfolio_id: str order_id: str execution_id: str side: str diff --git a/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/design.md b/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/design.md new file mode 100644 index 0000000..c83681c --- /dev/null +++ b/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/design.md @@ -0,0 +1,32 @@ +# BAT-015 Design + +- Status: confirmed +- Batch: BAT-015 + +## Architecture Summary +- REL-001 잔여 정합화 3건을 단일 batch로 묶어 처리한다. DB schema 마이그레이션 1건(REQ-033), Pydantic 스키마 강화 1건(REQ-034), Health 응답 + baseline 문서 갱신 1건(REQ-035). 변경 면적은 좁지만 cross-cutting nature(데이터 + 인증 + 운영)로 한 batch에 응집된다. + +## Changed Areas +- Docs: `docs/data-model.md`(ledger_trades 컬럼), `docs/interface-contract.md`(/health 응답), `docs/runtime-flows.md`(health probe 의미 명문화 — baseline 갱신) +- Modules: `app/repositories/postgres.py`(ledger_trades SQL), `app/domain/models.py`(LedgerTrade dataclass, 있다면), `app/api/v1_portfolios.py`(ledger handler 응답 매핑, 있다면), `app/schemas/auth.py`(LoginRequest model_config), `app/api/health.py`(HealthResponse + handler), `migrations/versions/`(신규 revision) +- Interfaces: `POST /v1/portfolios/{id}/ledger/trades` 응답 schema 일관성, `POST /auth/login` 422 거부 동작, `GET /health` 응답 확장 + +## Key Decisions +1. **REQ-033 마이그레이션 전략**: 단일 `ALTER TABLE ledger_trades RENAME COLUMN asset_id TO portfolio_id` 트랜잭션 사용. 별도 컬럼 추가 + backfill + drop 패턴은 채택하지 않음(데이터 의미가 이미 portfolio_id로 일관되어 backfill 불요, dcy-005 A-1 가정 + 사용자 confirm). +2. **REQ-034 enforcement 위치**: `LoginRequest.model_config = ConfigDict(extra="forbid")` 스키마 단계에서 처리. `AuthService.login`에 별도 검증 로직 추가 안 함(스키마 단계에서 Pydantic 표준 422 응답이 더 명확). +3. **REQ-035 db_enabled 의미**: `settings.db_enabled`(환경변수) 값 노출. actual DB ping은 본 batch 범위 밖이며 `docs/runtime-flows.md`에 의미 명문화로 해소. +4. **baseline 갱신 위치**: `docs/runtime-flows.md`의 "Health Probe Flow"(또는 유사 섹션)에 `db_enabled` 의미 노트 1단락 추가. `docs/project-structure.md`는 영향 없음. +5. **마이그레이션 revision 번호**: 기존 head(`20260521_0023`) 다음으로 `20260601_NNNN_ledger_trades_portfolio_id_rename.py`. revision 본문은 `op.execute("ALTER TABLE ... RENAME COLUMN ...")` 또는 `op.alter_column(...)` 패턴. + +## Edge Cases +- REQ-034: 기존 `LoginRequest`에 정의된 필드만 보내는 client는 영향 없음. `username`/`password` 외 어떤 키든 422로 거부됨. +- REQ-035: `PORTFOLIOHUB_DB_ENABLED` 환경변수가 미설정(default `false`)인 경우 `db_enabled: false` 응답. 본 워크스페이스 `.env`는 `true`로 설정되어 있음. +- REQ-033: `ledger_trades`에 데이터가 없는 환경(개발 초기)에서도 ALTER COLUMN RENAME은 즉시 완료. 대량 데이터 환경 staging은 dry-run에서 측정. +- 3건 통합: 동일 release에 묶일 예정이지만 각 변경이 서로 의존하지 않으므로 implementation 순서 자유. planning에서 권장 순서(REQ-034 → REQ-035 → REQ-033) 명시. + +## Architecture Impact +- runtime-flows + +## Reference Doc Update Plan +- project-structure.md: none (구조 변경 없음, 기존 모듈에 한정된 변경) +- runtime-flows.md: REQ-035에서 `/health` probe flow 섹션에 "`db_enabled`는 환경변수 `PORTFOLIOHUB_DB_ENABLED` 설정값 노출이며 actual DB connectivity ping이 아니다. production-grade readiness probe는 별도 `/readiness` endpoint 분리로 후속 처리"의 의미 명문화 노트 1단락 추가 diff --git a/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/implementation.md b/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/implementation.md new file mode 100644 index 0000000..4b00396 --- /dev/null +++ b/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/implementation.md @@ -0,0 +1,53 @@ +# BAT-015 Implementation + +- Status: completed +- Batch: BAT-015 + +## Plan Summary +- 변경 순서: REQ-034 → REQ-035 → REQ-033 (planning 권장 순서대로 진행) +- 각 REQ 구현 후 부분 회귀 (pytest 전체 + 해당 영역 smoke) +- 최종 batch 통합 회귀 + 로컬 smoke + +## Changed Files +- REQ-034: + - `app/schemas/auth.py` — `LoginRequest.model_config = ConfigDict(extra="forbid")` 추가 + - `tests/test_auth.py` — `test_login_rejects_no_expiry_extra_field`, `test_login_rejects_arbitrary_extra_field` 신규 +- REQ-035: + - `app/api/health.py` — `HealthResponse.db_enabled` 필드 추가 + `healthcheck()` 핸들러가 `settings.db_enabled` 노출 + - `tests/test_app.py` — `test_healthcheck`에 db_enabled 확인 추가, `test_health_exposes_db_enabled_true`, `test_health_exposes_db_enabled_false` 신규 + - `docs/runtime-flows.md` — health endpoint 섹션(line 97)에 `db_enabled` 의미 명문화 노트 추가 + - `docs/interface-contract.md` — `/health` payload schema에 `db_enabled` 필드 추가 + 의미 노트 +- REQ-033: + - `migrations/versions/20260601_0024_ledger_trades_portfolio_id_rename.py` — 신규 alembic revision (ALTER COLUMN RENAME with has_table guard) + - `app/domain/models.py` — `LedgerTrade.asset_id` → `portfolio_id` + - `app/repositories/portfolio_repository.py` — `insert_ledger_trade` interface 시그니처 갱신 + - `app/repositories/postgres.py` — `insert_ledger_trade` INSERT/SELECT/RETURNING SQL 컬럼명 + `_row_to_ledger_trade` 매핑 + - `app/schemas/v1.py` — `LedgerTradeResponse.asset_id` → `portfolio_id` + - `app/api/v1_portfolios.py` — ledger handler에서 `insert_ledger_trade(portfolio_id=...)` + 응답 매핑 + - `tests/test_v1_portfolios_ledger.py` — `LedgerTrade(portfolio_id=...)` + 응답 `body["portfolio_id"]` 검증 + - `tests/test_v1.py` — `_make_ledger_trade` + `body["portfolio_id"]` 검증 + - `tests/test_daemon_assignment.py` — mock `trade.portfolio_id` 갱신 + - `docs/data-model.md` — LedgerTrade entity, Key Fields, Uniqueness Constraints, State Rules의 `asset_id` 표기 모두 `portfolio_id`로 갱신 + revision 20260601_0024 명시 + - `docs/interface-contract.md` — `POST /v1/portfolios/{id}/ledger/trades` 응답 schema에 `portfolio_id` 명시 + REQ-033 cross-reference + +## Execution Log +1. REQ-034: `LoginRequest`에 `extra="forbid"` 적용 → `tests/test_auth.py` 16 passed +2. REQ-035: `HealthResponse` 확장 + 핸들러 → `tests/test_app.py` 9 passed → baseline 갱신 +3. REQ-033: alembic revision 20260601_0024 작성 → `alembic upgrade head` 적용(local DB에 `ledger_trades` 테이블 부재로 `has_table` guard에 의해 안전 skip) → repository/도메인/스키마/라우트 정합 → `tests/test_v1_portfolios_ledger.py` 10 passed +4. 통합 회귀 1차: 5 failed 발견 (test_v1.py + test_daemon_assignment.py 잔존 `asset_id` mock) → 갱신 → 통합 회귀 2차: **372 passed, 0 failed** +5. 로컬 smoke: + - GET /health → HTTP 200 + `db_enabled: true` (REQ-035 AC-1) + - POST /auth/login + `no_expiry: true` → HTTP 422 + `extra_forbidden` (REQ-034 AC-2) + - codebase grep `ledger_trades.*asset_id` → 0건 (REQ-033 AC-3) + - `docs/runtime-flows.md` baseline 노트 grep → 매치 (REQ-035 AC-4) + - `docs/interface-contract.md` /health payload grep → 매치 (REQ-035 AC-5) + +## Validation +- pytest 전체: **372 passed, 0 failed** (BAT-015 base 368 → +4: test_auth 2건 + test_app 2건 신규) +- 로컬 smoke 5건 모두 통과 +- AC 매핑은 verification.md에서 정리 +- 실 DB AC (REQ-033 AC-1 `\d ledger_trades`, AC-4 staging dry-run lock 측정): 본 워크스페이스 `ledger_trades` 테이블 미생성 상태로 N/A. REL-002 staging 시점에 별도 수집. + +## Remaining Risks +- R-실 DB ledger_trades 테이블 미생성: REQ-033 마이그레이션은 `has_table` guard로 안전하지만, 본 워크스페이스에서 ledger_trades 적재 흐름이 활용되지 않는 상태. BAT-011 release-candidate verification은 mock 기반이라 이 gap이 release 단계에서 발견되지 않았음. **본 batch 범위 밖**, 별도 후속 항목으로 등록 후보(REL-002 또는 후속 Discovery). +- (workspace_purpose 메모리: 본 워크스페이스 개발용 → prod 영향 검증 N/A) diff --git a/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/index.md b/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/index.md new file mode 100644 index 0000000..d29c524 --- /dev/null +++ b/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/index.md @@ -0,0 +1,34 @@ +# BAT-015: REL-001 Feedback 정합화 (ledger 컬럼명 / LoginRequest 엄격화 / health db_enabled) + +- Status: release-candidate +- Profile: standard +- Source Discovery: dcy-005 +- Included REQ: REQ-033, REQ-034, REQ-035 +- Excluded REQ: 없음 + +## Summary +- dcy-005(rel-001-feedback-cleanup) 산출물 3건을 단일 batch로 묶어 일괄 정합화한다. 3건 모두 변경 면적이 좁고 같은 release(REL-002 분리 예정) target으로 일관된 정합화 묶음 trigger를 가진다. + +## Scope +- In Scope: + - REQ-033: `ledger_trades.asset_id` → `portfolio_id` 컬럼명 정합화 (Alembic 신규 revision + postgres.py SQL 갱신 + 도메인 모델/라우트 정합) + - REQ-034: `LoginRequest`에 `model_config = ConfigDict(extra="forbid")` 적용 + pytest 422 회귀 + - REQ-035: `HealthResponse`에 `db_enabled: bool` 필드 추가 + `healthcheck()` 핸들러 갱신 + `docs/runtime-flows.md` baseline 명문화 + `docs/interface-contract.md` /health 응답 schema 갱신 +- Out of Scope: + - `ledger_trades` 의미 재정의(asset 단위 vs portfolio 단위 — REQ-033 portfolio_id 의미 확정 가정) + - session token TTL 정책 변경(REQ-021 본문 자체 갱신은 본 batch 범위 밖) + - `/readiness` endpoint 분리(production-grade readiness probe, 별도 후속 Discovery로 처리) + - REL-001 verification 재실행 (별도 REL-002로 분리 — dcy-005 OQ-3 결정) + +## Documents +- Planning: [planning.md](./planning.md) +- Design: [design.md](./design.md) +- Implementation: [implementation.md](./implementation.md) +- Verification: [verification.md](./verification.md) + +## Notes +- 공통 trigger: REL-001 Feedback Handoff 3건 정합화 묶음 +- 공통 release: REL-002 분리 배포 예정(dcy-005 OQ-3 결정, 2026-06-01) +- REQ-033 마이그레이션 staging dry-run을 verification 단계에서 수행해 운영 lock 시간 < 1초(NFR-1) 측정 +- REQ-035는 baseline 문서(`docs/runtime-flows.md`) 갱신 요구를 포함하므로 design 단계에서 architecture impact를 `runtime-flows`로 표기 +- 본 워크스페이스는 개발용이므로 prod 운영 영향 검증은 release 단계에서 별도 수행 (memory: workspace_purpose) diff --git a/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/planning.md b/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/planning.md new file mode 100644 index 0000000..2b641c7 --- /dev/null +++ b/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/planning.md @@ -0,0 +1,50 @@ +# BAT-015 Planning + +- Status: confirmed +- Batch: BAT-015 +- Profile: standard + +## Included REQ +- REQ-033 (Migration): ledger_trades.asset_id → portfolio_id 컬럼명 정합화 +- REQ-034 (Interface): LoginRequest 추가 필드 거부 enforcement +- REQ-035 (Interface): GET /health 응답에 db_enabled 필드 추가 + +## Delivery Plan +- 순서: REQ-034 → REQ-035 → REQ-033 + - REQ-034 우선: 1줄 schema 변경 + pytest 422 회귀 (저위험·빠른 evidence) + - REQ-035 후속: HealthResponse 확장 + baseline 문서 갱신 (저위험·문서 변경 포함) + - REQ-033 마지막: Migration 동반 (가장 큰 변경, alembic revision + DB schema, staging dry-run 필요) +- 선행 조건: + - dcy-005 confirmed (2026-06-01) ✓ + - REQ-033/034/035 Approved (2026-06-01) ✓ + - 본 워크스페이스 개발용 메모리 확인 — prod 영향 검증은 release 단계 ✓ +- batch 통합 회귀: 3건 변경 후 전체 pytest 회귀 + 로컬 smoke (`/health`, `/auth/login`, `/v1/portfolios/{id}/ledger/trades`) + +## Dependencies +- Internal: + - dcy-005: Discovery 본문에 OQ-1/OQ-2/OQ-3 결정 명문화 완료 + - REQ-021: 본 batch가 REQ-021 AC-5 follow-up을 해소(REQ-034). REQ-021 본문 변경은 본 batch 범위 밖이며 cross-reference만 추가. + - REQ-029: 본 batch가 REQ-029 Notes follow-up을 해소(REQ-033). REQ-029 본문 변경은 본 batch 범위 밖. + - REL-001: 별도 release로 분리 배포 진행 중. 본 batch는 REL-002로 분리. +- External: + - 없음 (외부 client/integration 의존 없음) + +## Design Gate +- Design Required Before Implementation: yes +- Reason: REQ-033이 DB schema 마이그레이션을 동반하고 REQ-035가 baseline 문서(`docs/runtime-flows.md`) 갱신을 요구한다. Architecture impact가 있으므로 design 단계에서 architecture 결정과 baseline 갱신 계획을 명문화한 뒤 implementation 진입. + +## Milestones +1. Design 단계: REQ-033 마이그레이션 전략 결정(ALTER COLUMN RENAME vs 별도 컬럼 추가+backfill), REQ-035 baseline 문서 갱신 위치 결정 +2. Implementation 단계: REQ-034 → REQ-035 → REQ-033 순서로 구현 +3. Verification 단계: pytest 회귀(전체 369+ passed 기대) + REQ-033 staging dry-run + 로컬 smoke (`/health` 응답, `/auth/login` 422, `/v1/portfolios/{id}/ledger/trades` 회귀) +4. confirm-batch-verification 후 confirm-req-implemented 3건 일괄 전환 + +## Risks +- R-1 (Low): REQ-033 ALTER COLUMN RENAME이 ledger_trades 테이블에 lock을 유발해 운영 영향 발생. + - 대응: staging dry-run으로 lock 시간 측정(< 1초 권장), 짧은 단일 트랜잭션 패턴 적용. 본 워크스페이스는 개발용이라 prod 영향 검증은 release 단계. +- R-2 (Low): REQ-034 `extra="forbid"`가 기존 ignored field를 보내던 외부 client를 silently 깨뜨림. + - 대응: 본 워크스페이스 개발용이라 외부 monitoring 영향 없음. 회귀 테스트에서 기존 username/password만 보내는 케이스 통과 확인. +- R-3 (Low): REQ-035 `HealthResponse` 확장이 strict JSON schema validation 사용 monitoring 도구를 깨뜨림. + - 대응: 본 워크스페이스 개발용이라 영향 없음. prod 배포 시 사전 점검(REL-002 verification). +- R-4 (Low): 3건 변경이 통합 회귀에서 예상치 못한 상호 영향 발생. + - 대응: 각 REQ 구현 후 pytest 회귀 incremental 실행, batch 마지막에 전체 회귀. diff --git a/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/verification.md b/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/verification.md new file mode 100644 index 0000000..d742f43 --- /dev/null +++ b/docs/batches/bat-015_20260601_rel-001-feedback-cleanup/verification.md @@ -0,0 +1,60 @@ +# BAT-015 Verification + +- Status: confirmed +- Batch: BAT-015 + +## Acceptance Mapping + +### REQ-033: ledger_trades.asset_id → portfolio_id 컬럼명 정합화 +- AC-1 (`\d ledger_trades`에 `portfolio_id` 존재, `asset_id` 부재): **Deferred to REL-002 staging** — 로컬 DB에 `ledger_trades` 테이블이 미생성 상태(BAT-011 release-candidate는 mock 기반 verification으로 진행됨). 마이그레이션 `20260601_0024`는 `has_table` 가드로 안전 skip되며, 코드 작성은 완료([migrations/versions/20260601_0024_ledger_trades_portfolio_id_rename.py:36-40](../../../migrations/versions/20260601_0024_ledger_trades_portfolio_id_rename.py#L36-L40)). 실 DB 검증은 REL-002 staging에서 수행. +- AC-2 (INSERT/SELECT 회귀): `tests/test_v1_portfolios_ledger.py` 10 PASSED (mock 기반). `insert_ledger_trade(portfolio_id=...)` 시그니처 + 응답 schema `LedgerTradeResponse.portfolio_id` 일관 검증. +- AC-3 (codebase grep `ledger_trades\..*asset_id` 0건): PASSED. `grep -rn "ledger_trades\..*asset_id\|asset_id.*ledger_trades\|insert_ledger_trade.*asset_id\|trade\.asset_id" app/` → 0건 (2026-06-05 implementation smoke). +- AC-4 (마이그레이션 lock < 1초, NFR-1): **Deferred to REL-002 staging** — workspace_purpose 메모리 정합(개발용 워크스페이스). staging dry-run으로 측정. +- AC-5 (전체 pytest 회귀 통과): PASSED. **372 passed, 0 failed** (5월 21일 base 368 → +4 누적: test_auth +2, test_app +2). + +### REQ-034: LoginRequest 추가 필드 거부 enforcement +- AC-1 (정상 username/password → HTTP 200): `tests/test_auth.py` 기존 회귀 16 PASSED (정상 login flow는 기존 케이스로 회귀 보호). +- AC-2 (`no_expiry: true` → HTTP 422 + `extra_forbidden`): `tests/test_auth.py::test_login_rejects_no_expiry_extra_field` PASSED. 로컬 smoke 응답 `{"detail":[{"type":"extra_forbidden","loc":["body","no_expiry"],"msg":"Extra inputs are not permitted","input":true}]}` 확인 (2026-06-05). +- AC-3 (임의 추가 필드 → HTTP 422): `tests/test_auth.py::test_login_rejects_arbitrary_extra_field` PASSED. +- AC-4 (grep `extra.*forbid` app/schemas/auth.py 매치): PASSED. [app/schemas/auth.py:14](../../../app/schemas/auth.py#L14) `model_config = ConfigDict(extra="forbid")` 매치. +- AC-5 (전체 pytest 회귀 통과): PASSED. 372 passed, 0 failed. + +### REQ-035: GET /health 응답에 db_enabled 필드 추가 +- AC-1 (PORTFOLIOHUB_DB_ENABLED=true → `db_enabled: true`): `tests/test_app.py::test_health_exposes_db_enabled_true` PASSED. 로컬 smoke 응답 `{"status":"ok","service":"portfolio-hub","version":"0.1.0","checked_at":"2026-06-05T13:44:04.724864+09:00","db_enabled":true}` 확인 (2026-06-05). +- AC-2 (PORTFOLIOHUB_DB_ENABLED=false → `db_enabled: false`): `tests/test_app.py::test_health_exposes_db_enabled_false` PASSED. +- AC-3 (5필드 노출 + 기존 4필드 회귀): `tests/test_app.py::test_healthcheck` 갱신 PASSED (`db_enabled` 추가 검증 포함). 기존 `status/service/version/checked_at` 동작 회귀 유지. +- AC-4 (baseline grep `db_enabled.*설정값|setting value|ping|readiness` docs/runtime-flows.md): PASSED. [docs/runtime-flows.md:97](../../../docs/runtime-flows.md#L97)에 의미 명문화 노트 1단락 추가. +- AC-5 (interface-contract grep `/health.*db_enabled` docs/interface-contract.md): PASSED. [docs/interface-contract.md:48](../../../docs/interface-contract.md#L48) `GET /health payload` schema에 `db_enabled` 필드 추가. + +## Evidence + +- Test: + - 전체 pytest: **372 passed, 0 failed, 3 warnings** (2026-06-05, deprecation warnings는 starlette HTTP_422_UNPROCESSABLE_ENTITY alias, 기능 무관) + - 신규 회귀 4건: `test_login_rejects_no_expiry_extra_field`, `test_login_rejects_arbitrary_extra_field`, `test_health_exposes_db_enabled_true`, `test_health_exposes_db_enabled_false` + - 영향 받은 기존 회귀 갱신: `test_v1_portfolios_ledger.py` (portfolio_id 응답 필드), `test_v1.py::test_v1_ledger_trade_*` (`_make_ledger_trade` + body 키), `test_daemon_assignment.py::test_ledger_trade_201_when_daemon_matches` (mock attribute), `test_app.py::test_healthcheck` (`db_enabled` 필드 확인 추가) +- Manual: + - 2026-06-05 13:44 KST 로컬 smoke 5건 (implementation.md Execution Log step 5 참조): + - GET /health → 200 + `db_enabled: true` + - POST /auth/login + `no_expiry: true` → 422 + extra_forbidden + - codebase grep ledger_trades.asset_id → 0건 + - docs/runtime-flows.md db_enabled 의미 노트 grep → 매치 + - docs/interface-contract.md /health payload `db_enabled` grep → 매치 +- Logs: + - alembic upgrade head (2026-06-05): `Running upgrade 20260521_0023 -> 20260601_0024, Rename ledger_trades.asset_id to portfolio_id for column-name semantic alignment (REQ-033)` — `has_table` guard로 안전 skip 확인 + +## Architecture Impact Synchronization (Baseline) + +- design.md Architecture Impact: `runtime-flows` +- Reference Doc Update Plan 이행: + - `docs/project-structure.md`: 갱신 불요 (영향 없음, 기존 모듈 한정 변경) — 변경 없음 확인 ✓ + - `docs/runtime-flows.md`: `db_enabled` 의미 명문화 노트 [line 97](../../../docs/runtime-flows.md#L97) 추가 ✓ +- 추가 baseline 갱신: `docs/data-model.md` LedgerTrade entity 컬럼명/제약 갱신, `docs/interface-contract.md` /health payload + LedgerTradeResponse schema 갱신 + +## Result +- Ready for Release: true +- Blocking Issues: + - 없음 (Deferred 2건은 blocker 아님, REL-002 staging에 이연 evidence) +- Deferred Evidence (release execution 시 수집): + - REQ-033 AC-1 (`\d ledger_trades` 실 DB 컬럼 검증) — REL-002 staging + - REQ-033 AC-4 (마이그레이션 lock < 1초 staging dry-run) — REL-002 staging + - 본 워크스페이스 ledger_trades 테이블 미생성 상태 자체는 본 batch 범위 밖 (BAT-011 mock 기반 verification의 후속 항목 후보) diff --git a/docs/batches/index.md b/docs/batches/index.md index a7964c7..0a640f5 100644 --- a/docs/batches/index.md +++ b/docs/batches/index.md @@ -22,3 +22,4 @@ | BAT-012 | standard | release-candidate | REQ-002, REQ-010 | dcy-001, dcy-002, dcy-003 | [bat-012](./bat-012_20260521_assets-tagging-and-admin-get-redefinition/) | | BAT-013 | batch-lite | release-candidate | REQ-005 | dcy-002 | [bat-013](./bat-013_20260521_asset-tag-apply-remove-fix/) | | BAT-014 | batch-lite | release-candidate | REQ-025, REQ-026, REQ-027, REQ-028, REQ-032 | dcy-003 | [bat-014](./bat-014_20260524_portfolioitem-symbol-response-contract/) | +| BAT-015 | standard | release-candidate | REQ-033, REQ-034, REQ-035 | dcy-005 | [bat-015](./bat-015_20260601_rel-001-feedback-cleanup/) | diff --git a/docs/data-model.md b/docs/data-model.md index 9694b33..d96a924 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -261,14 +261,14 @@ ## Entity: `LedgerTrade` -- Purpose: record idempotent trade execution entries submitted by trex-basic via POST /v1/assets/{asset_id}/ledger/trades -- Source of Truth: ledger_trades table (added by migration 20260502_0014) -- Lifecycle: inserted once per unique (asset_id, order_id, execution_id) tuple; never updated or deleted +- Purpose: record idempotent trade execution entries submitted by trex-basic via POST /v1/portfolios/{portfolio_id}/ledger/trades +- Source of Truth: ledger_trades table (added by migration 20260502_0014; column `asset_id` renamed to `portfolio_id` by migration 20260601_0024 — REQ-033 / dcy-005 FR-1) +- Lifecycle: inserted once per unique (portfolio_id, order_id, execution_id) tuple; never updated or deleted ### Key Fields - ledger_entry_id :: bigint (Identity) :: auto-generated sequential primary key -- asset_id :: text :: portfolio_id (UUID-format text after BAT-006) referencing the trading portfolio +- portfolio_id :: text :: portfolios.portfolio_id (UUID-format text) referencing the trading portfolio (renamed from `asset_id` by REQ-033) - order_id :: text :: broker-assigned order identifier - execution_id :: text :: broker-assigned execution/fill identifier - side :: text :: "buy" or "sell" @@ -281,14 +281,14 @@ ### Uniqueness Constraints -- uq_ledger_trades_idempotency :: UNIQUE (asset_id, order_id, execution_id) :: idempotency guarantee; duplicate inserts are detected and return HTTP 200 + status="duplicate" -- idx_ledger_trades_asset_executed :: INDEX (asset_id, executed_at) :: time-range query optimization +- uq_ledger_trades_idempotency :: UNIQUE (portfolio_id, order_id, execution_id) :: idempotency guarantee; duplicate inserts are detected and return HTTP 200 + status="duplicate" +- idx_ledger_trades_asset_executed :: INDEX (portfolio_id, executed_at) :: time-range query optimization (constraint/index objects retain their original names; only the underlying column was renamed) ### State Rules - Rows are immutable after insertion; no UPDATE path exists. - Payload conflict detection: if unique key matches but payload differs → HTTP 409 Conflict. -- asset_id is TEXT (not a FK) but must correspond to a valid portfolios.portfolio_id before insertion. +- portfolio_id is TEXT (not a FK) but must correspond to a valid portfolios.portfolio_id before insertion. ## Shared Consistency Rules diff --git a/docs/discovery/dcy-005_20260601_rel-001-feedback-cleanup.md b/docs/discovery/dcy-005_20260601_rel-001-feedback-cleanup.md new file mode 100644 index 0000000..ce8326a --- /dev/null +++ b/docs/discovery/dcy-005_20260601_rel-001-feedback-cleanup.md @@ -0,0 +1,224 @@ +# 0. 문서 상태 +이 문서의 현재 상태와 식별 정보를 기록한다. 문서 추적과 승인 흐름에 필요한 최소 메타데이터를 적는다. + +- 상태: confirmed +- 문서 ID: dcy-005_20260601_rel-001-feedback-cleanup +- 이슈명: REL-001 Feedback 정합화 — ledger_trades.asset_id 명명 / session no_expiry enforcement / health db_enabled +- 작성 시각(KST): 2026-06-01 17:00 +- 마지막 갱신 시각(KST): 2026-06-01 19:00 +- 대체됨: 없음 +- 후속 Discovery 참조: 없음 +- 생성된 REQ 참조: REQ-033 ([req-033](../srs/Migration/req-033_ledger-trades-asset-id-to-portfolio-id-rename.md)), REQ-034 ([req-034](../srs/Interface/req-034_login-request-extra-forbid-enforcement.md)), REQ-035 ([req-035](../srs/Interface/req-035_health-response-db-enabled-field.md)) + +# 1. 계획 상태 요약 +현재 Discovery가 어떤 배경에서 작성되고 있는지, 연계 문서가 있는지, 지금 시점의 해석이 무엇인지 짧게 요약한다. + +- 연계 문서 현황: + - 관련 점수 파일: 미생성 + - 관련 TODO/메모: [rel-001 Feedback Handoff](../releases/rel-001_20260521_v1-api-full-surface.md#L107-L126)에 등록된 3건의 follow-up 후보 +- 해석: + - REL-001 배포 준비 단계에서 누적된 3건의 잔여 정합화 항목(`ledger_trades.asset_id` 컬럼명 혼용, session no_expiry 코드 enforcement 누락, `/health` 응답 모델 `db_enabled` 부재)을 한 묶음 Discovery로 추적해 다음 batch에서 일괄 정리한다. + - 3건 모두 작은 범위 변경이며 backwards-compat 영향이 제한적이므로 단일 Discovery + 단일 batch(Lite 또는 표준)로 처리한다. REL-001은 현 상태로 배포 진행하고 dcy-005 3건은 **별도 REL-002로 분리**(OQ-3 결정, 2026-06-01). + +# 2. 요구사항 판정 결과 +이번 요청이 기존 요구사항과 비교해 신규인지, 유사한지, 중복인지 판단하고 그 근거를 남긴다. + +- 판정: 신규 +- 근거 + - 3건 모두 기존 REQ(REQ-021, REQ-029) Notes/follow-up에 등록된 잔여 항목이며 별도 REQ로 분해된 적이 없다. + - 정합화 묶음이라는 trigger(REL-001 Feedback 일괄 정리)는 기존 Discovery에 없다. + +# 3. 문제점의 요약 +문제의 현재 상태, 기대 상태, 관련 이해관계자, 제약사항을 정리한다. Discovery의 핵심 맥락을 가장 먼저 이해할 수 있어야 한다. + +- 현재 상태 + - **3건-1 `ledger_trades.asset_id` 컬럼명 혼용**: REQ-029 구현에서 ledger 경로를 `/v1/portfolios/{id}/ledger/trades`로 이전했으나, `ledger_trades` 테이블의 `asset_id` 컬럼은 실제로는 portfolio_id를 저장한다([app/repositories/postgres.py:1284-1294](../../app/repositories/postgres.py#L1284-L1294) INSERT 컬럼 순서 + RETURNING). 컬럼명과 의미가 일치하지 않아 schema reader가 혼동. + - **3건-2 session no_expiry 코드 enforcement 누락**: REQ-021 정책 본문([docs/srs/Documentation/req-021_token-expiry-and-rotation-policy.md:15-17](../srs/Documentation/req-021_token-expiry-and-rotation-policy.md#L15-L17))은 "`no_expiry`는 service 한정, session/client에서 시도하면 HTTP 422 또는 ValueError로 거부"로 명시. 그러나 현재 `AuthService.login`([app/services/auth_service.py:47-65](../../app/services/auth_service.py#L47-L65))과 `LoginRequest` 스키마([app/schemas/auth.py:13-23](../../app/schemas/auth.py#L13-L23))에는 `no_expiry` 옵션 자체가 존재하지 않아 실질적으로 시도 불가하지만, 정책 명세 ↔ 코드 매핑(REQ-021 AC-5)에서 "명시적 거부 로직 미구현" follow-up이 등록됨. + - **3건-3 `/health` 응답에 `db_enabled` 필드 미노출**: `HealthResponse` 모델([app/api/health.py:17-21](../../app/api/health.py#L17-L21))이 `status`, `service`, `version`, `checked_at` 4필드만 노출. REL-001 verification 체크리스트는 "GET /health → HTTP 200 + `db_enabled: true`"를 요구하지만 응답 모델에 해당 필드 없음 — 외부 health probe가 DB 연결 상태를 직접 관찰할 수 없는 gap. +- 기대 상태 + - 3건-1: 컬럼명을 의미와 일치시키거나(`portfolio_id`로 rename), 또는 ledger_trades가 정말 asset 단위로 의미가 있는지 재정의. 마이그레이션 동반. + - 3건-2: session 발급 경로에서 `no_expiry` 시도 자체를 거부하는 명시적 enforcement 코드 추가(현재는 옵션 부재로 시도 불가지만 정책 명문화 완결성). 또는 정책 본문에 "현재 코드 구조상 시도 불가, 명시적 거부 로직 불요"로 갱신. + - 3건-3: `HealthResponse`에 `db_enabled: bool` 필드 추가 또는 별도 `/readiness` endpoint 분리. 어느 경로로 갈지 결정. +- 이해관계자 + - Backend 팀 — 코드 변경 및 마이그레이션 책임 + - Operations — `/health` 응답 변경 시 health probe 동작 검증 + - REL-001 배포 이후 trex-basic 등 ledger 호출 클라이언트 — `ledger_trades.asset_id` 컬럼명 변경 시 외부 query 의존성 확인 필요(있는 경우) +- 제약사항 + - REL-001은 현 상태로 배포 진행, dcy-005 결과물은 별도 REL-002로 분리(OQ-3 결정, 2026-06-01). REL-001 verification 재실행 불요. + - 마이그레이션을 동반하면 dcy-004 / REQ-017 패턴(CREATE INDEX CONCURRENTLY 등)과 정합 유지. REL-001 마이그레이션 chain(`20260521_0023`) 다음 revision으로 자연스럽게 추가. + - `/health` 응답 schema 변경은 backwards-compat 영향 — 기존 client(probe, monitoring)가 추가 필드를 허용하는지 확인. + +## 주요 사용자 시나리오 +사용자 또는 운영자가 실제로 어떤 상황에서 이 변경을 필요로 하는지 대표 시나리오를 적는다. + +- DB schema reader가 `ledger_trades.asset_id`를 보고 assets 테이블 참조라고 오해해 잘못된 join query 작성. +- 정책 감사자가 REQ-021 vs 실제 코드를 매핑할 때 session에서 `no_expiry` 거부가 코드에 명시적으로 없어 명확성 부족으로 audit finding 작성. +- 운영자가 prod에서 health probe 응답을 monitoring 도구로 받을 때 DB 연결 끊김을 빠르게 감지하지 못함(현재는 status: ok만 노출). + +# 4. 이번에 정의할 변경 +이번 Discovery에서 다루는 핵심 변경 항목을 목록으로 적는다. 기능, 구조, 규칙 변경을 함께 포함해도 된다. + +- `ledger_trades.asset_id` 컬럼명을 의미에 맞게 `portfolio_id`로 변경하는 마이그레이션 + 코드 경로 정합화. +- `LoginRequest`에 `model_config = ConfigDict(extra="forbid")` 적용으로 session `no_expiry` 시도 자체를 HTTP 422로 거부 (OQ-1 결정, 2026-06-01). +- `HealthResponse` 모델에 `db_enabled: bool` 필드 추가 + `healthcheck()` 핸들러가 `settings.db_enabled` 노출 (OQ-2 결정, 2026-06-01). + +## 영향 범위 +변경이 미칠 코드/문서/운영 범위를 명시해 REQ 정제와 batch planning에서 추정 누락을 줄인다. + +- 코드 영향: + - 3건-1: `app/repositories/postgres.py` ledger_trades INSERT/SELECT 컬럼 변경, `app/domain/models.py` LedgerTrade dataclass 필드명 (있다면), `app/api/v1_portfolios.py` ledger route handler, `migrations/versions/` 신규 revision (rename column with backfill). + - 3건-2: `app/schemas/auth.py:LoginRequest` — `model_config = ConfigDict(extra="forbid")` 추가 (OQ-1 결정 (a) 경로). `app/services/auth_service.py:login`은 변경 없음. + - 3건-3: `app/api/health.py` HealthResponse에 `db_enabled: bool` 필드 추가 + `healthcheck()` 핸들러가 `settings.db_enabled` 노출 (OQ-2 결정 (a) 경로). 별도 `/readiness` router 신설은 본 Discovery 범위 밖. +- 문서 영향: + - 3건-1: `docs/data-model.md` ledger_trades 컬럼 명세, `docs/interface-contract.md` ledger 응답 schema(asset_id가 응답에 노출되는 경우) + - 3건-2: `docs/srs/Documentation/req-021_token-expiry-and-rotation-policy.md` (선택 시 본문 갱신), `docs/runtime-flows.md` Token Expiry 섹션 + - 3건-3: `docs/interface-contract.md` /health 응답 schema(`db_enabled` 추가), `docs/runtime-flows.md` health probe flow에 "db_enabled는 설정값 노출, actual ping 아님" 명문화 (OQ-2 결정 (a) 경로) + - 3건 공통: 본 Discovery + 후속 REQ + (선택) baseline 문서 정합 +- 운영 영향: + - 3건-1: 마이그레이션 실행 시 ledger_trades 테이블 잠금 영향. CREATE INDEX CONCURRENTLY 패턴 적용 또는 짧은 ALTER COLUMN RENAME 트랜잭션. + - 3건-2: `extra="forbid"` 적용으로 LoginRequest 전체에 추가 필드 거부 효과. 기존 client(현재는 username/password만 전송)는 영향 없음. 향후 외부 client가 ignored field를 보내고 있었다면 422로 깨질 수 있으나, 본 워크스페이스는 개발용이라 영향 무. + - 3건-3: health probe 응답 필드 확장 — 본 워크스페이스는 개발용이라 외부 monitoring 영향 없음. 미래 prod 도입 시 도구가 추가 필드를 허용하는지 사전 확인. + +# 5. 요구사항 목록 +실제로 구현과 검증으로 이어질 요구사항을 기능/비기능으로 나누어 정리하고, 범위 경계를 명확히 적는다. + +## 기능 요구사항 +- FR-1: `ledger_trades.asset_id` 컬럼을 `portfolio_id`로 rename하는 Alembic 마이그레이션을 작성한다. 기존 데이터는 동일 의미(portfolio_id 저장)이므로 backfill 불요. INSERT/SELECT/RETURNING SQL 및 LedgerTrade dataclass 필드명도 함께 정합화한다. +- FR-2: REQ-021의 "session에서 `no_expiry` 거부" 정책을 `LoginRequest` 스키마 단계에서 코드 enforcement로 명시적 거부한다(OQ-1 결정, 2026-06-01 — (a) 경로). 구현 방법: `LoginRequest`에 `model_config = ConfigDict(extra="forbid")` 적용 또는 별도 validator로 `no_expiry` 패턴 거부. 외부 client가 LoginRequest에 정의되지 않은 필드(`no_expiry` 포함)를 전송하면 HTTP 422로 거부된다. pytest 회귀 케이스 1건(TestClient `/auth/login` body에 `no_expiry: true` 포함 → 422 확인) 신설. +- FR-3: `/health` 응답 본문에 `db_enabled: bool` 필드를 노출한다(OQ-2 결정 (a) 경로, 2026-06-01). `HealthResponse` 모델에 `db_enabled: bool` 필드 추가 + `healthcheck()` 핸들러가 `settings.db_enabled` 값을 그대로 채운다. **본 값은 환경변수 `PORTFOLIOHUB_DB_ENABLED` 설정값을 노출하는 것이며 실제 DB connectivity ping이 아님**을 docs/runtime-flows.md(또는 응답 모델 docstring)에 명문화한다. 미래 production monitoring 정착 시 `/readiness` endpoint 분리는 별도 Discovery로 분리 가능. + +## 비기능 요구사항 +성능, 운영성, 일관성, 자동화 안정성 등 기능 외 요구사항을 적는다. + +- NFR-1: 3건 모두 backwards-compat 영향 최소화. 마이그레이션(FR-1)은 짧은 ALTER COLUMN RENAME으로 처리하고 운영 lock 시간을 < 1초 권장. /health 응답 확장(FR-3)은 필드 추가형으로 기존 client 영향 없음. +- NFR-2: 본 Discovery 결과물(후속 batch verification)은 자동 테스트로 검증 가능해야 한다. ledger_trades column rename은 마이그레이션 적용 후 INSERT/SELECT 회귀, `/health` 확장은 응답 본문 schema 검증, no_expiry enforcement는 `/auth/login` body에 추가 필드 포함 시 HTTP 422 회귀. + +## 범위 경계 +이번 반복에서 반드시 포함할 범위와 명시적으로 제외할 범위를 구분한다. + +- In Scope + - `ledger_trades.asset_id` 컬럼명 정합화 (rename + 코드/스키마 갱신) + - session `no_expiry` 코드 enforcement (`LoginRequest` extra="forbid") — OQ-1 결정 (a) + - `HealthResponse`에 `db_enabled: bool` 필드 추가 (settings 값 노출, OQ-2 결정 (a)) + - 위 3건의 마이그레이션 + 회귀 테스트 +- Out of Scope + - ledger_trades 의미 재정의(asset 단위 vs portfolio 단위 — 본 REQ는 portfolio_id 의미 확정 가정) + - session token TTL 정책 변경 (REQ-021 본문 자체 정책 갱신은 본 Discovery 범위 밖) + - prod 운영 health probe 도구 변경 (운영 영역) + - prod admin token rotation 등 보안 hotfix (본 워크스페이스는 개발용 — 참조 메모리) + +# 6. 리스크/가정 목록 +불확실성과 전제 조건을 분리해서 적는다. 리스크는 영향과 완화 방안을, 가정은 현재 판단의 전제를 기록한다. + +## 리스크 +- R-1 (Low): `ledger_trades.asset_id` 컬럼명 변경 시 외부 client(trex-basic 등)가 컬럼명을 직접 query하면 깨질 수 있음. + - 완화 방안: trex-basic은 HTTP API(POST /v1/portfolios/{id}/ledger/trades)만 호출하므로 DB 컬럼명 의존 없음 추정. 마이그레이션 전 외부 query 의존성 확인. +- R-2 (Low): `/health` 응답 본문 확장 시 monitoring 도구가 strict JSON schema validation을 사용하면 깨질 수 있음. + - 완화 방안: 본 워크스페이스는 개발용이라 외부 monitoring 영향 없음. prod 배포 시 사전 점검. +- R-3 (Low): 마이그레이션 적용 순서가 잘못되어 ledger_trades INSERT가 일시 실패할 수 있음. + - 완화 방안: 배포 전 staging에서 dry-run, 또는 ALTER COLUMN RENAME과 코드 배포를 같은 release window에 묶음. + +## 가정 +아직 확정되지 않았지만 현재 문서 작성의 기반이 되는 전제를 적는다. + +- A-1: `ledger_trades.asset_id`가 저장하는 값은 portfolio_id로 일관됨(과거 다른 의미가 섞이지 않음). REQ-029 구현 시점부터 portfolio_id를 저장한 것으로 확인. **사용자 confirm-discovery(2026-06-01)로 동의 확정**. +- A-2: 본 워크스페이스는 개발용이며 prod 배포 영향 검증은 release 단계에서 별도 수행. 본 Discovery 범위는 코드/문서/마이그레이션 작성에 한정. +- A-3: 3건은 같은 batch로 묶어 처리 가능하다(서로 독립적 변경이라 상호 충돌 없음). **사용자 confirm-discovery(2026-06-01)로 동의 확정**. + +## 오픈 질문 +답을 아직 모르는 항목을 적는다. 추정으로 덮지 말고 REQ 초안 작성 이전에 확인할 질문으로 남긴다. + +- OQ-1: session `no_expiry` 처리를 (a) 코드 enforcement 추가, (b) REQ-021 본문 갱신("코드 구조상 시도 불가") 중 어느 경로로 갈지. + - 상태: Answered (2026-06-01) + - 결정: (a) 코드 enforcement. `LoginRequest`에 `model_config = ConfigDict(extra="forbid")` 적용으로 정의되지 않은 필드(`no_expiry` 포함)를 HTTP 422로 거부. 정책 명문(REQ-021) 그대로 유지, 미래 안전망 효과 확보. + - 본문 반영: FR-2, # 4 코드/운영 영향, # 7 S-2 측정 방식, # 10 DECIDE 섹션에서 항목 제거 +- OQ-2: `/health` 응답에 `db_enabled` 추가 방식을 (a) HealthResponse 필드 추가, (b) 별도 `/readiness` endpoint 분리 중 어느 경로로 갈지. + - 상태: Answered (2026-06-01) + - 결정: (a) HealthResponse 필드 추가. `settings.db_enabled`(환경변수 PORTFOLIOHUB_DB_ENABLED 설정값)를 응답에 노출. **본 값은 설정값이며 actual DB connectivity ping이 아님**을 docs/runtime-flows.md 및 응답 모델 docstring에 명문화한다. 본 워크스페이스가 개발용이므로 Kubernetes-grade readiness 분리는 over-spec. 미래 production monitoring 정착 시 `/readiness` 분리는 별도 Discovery로 분리 가능. + - 본문 반영: FR-3, # 4 코드/문서/운영 영향, # 7 S-3 측정 방식, # 10 DECIDE 섹션에서 항목 제거 +- OQ-3: `ledger_trades.asset_id` rename 마이그레이션을 REL-001 배포 전/후 어디에 넣을지(별도 release 또는 REL-001 후속). + - 상태: Answered (2026-06-01) + - 결정: 별도 **REL-002**로 분리. REL-001은 현 상태(BAT-001~014 + REQ-028 augmentation, verification 완료, smoke 통과)로 배포 진행. dcy-005 3건은 신규 batch(BAT-015 가정) + REL-002로 분리해 sequence를 명확히 한다. + - 사유: REL-001 verification 안정성 최우선, 마이그레이션 sequence 분리, dcy-005가 "REL-001 잔여 정리" 묶음 trigger로 독립 추적 가치. REL-001 verification gap(`/health db_enabled` 미노출 [~] 부분 통과)은 우회 검증으로 blocker 아님. + - 본문 반영: # 1 해석, # 3 제약사항, # 10 DECIDE 섹션에서 항목 제거 + +### 오픈 질문 처리 기준 +- `Open`: 아직 답이 없고 다음 단계 진행에 영향이 있는 상태다. 문서에 유지한다. +- `Answered`: 답이 확정된 상태다. 관련 본문/요구사항에 반영한 뒤 오픈 질문 목록에서는 제거하거나 resolved 기록으로 이동한다. +- `Deferred`: 지금 단계에서 결정하지 않고 다음 단계 또는 후속 이슈로 넘기는 상태다. 넘길 대상과 이유를 반드시 적는다. +- `Dropped`: 더 이상 유효하지 않은 질문이다. 삭제 사유를 짧게 남기고 목록에서 제거한다. + +# 7. 초기 성공 기준 +이 변경이 성공했다고 판단할 기준을 측정 가능하게 정의한다. 가능하면 데이터 출처까지 연결한다. + +- S-1: `ledger_trades` 테이블에 `portfolio_id` 컬럼이 존재하고 `asset_id` 컬럼이 더 이상 사용되지 않는다(코드 grep + DB \d 검증). 기존 INSERT/SELECT 회귀 테스트 모두 통과. +- S-2: REQ-021 정책과 실제 코드가 일치한다 — `/auth/login` body에 `no_expiry: true`(또는 LoginRequest에 정의되지 않은 추가 필드)를 포함해 시도 시 HTTP 422 응답 (OQ-1 결정 (a) 경로, 2026-06-01). +- S-3: `GET /health` 응답이 `db_enabled` 필드를 직접 노출하고 그 값이 `settings.db_enabled`와 일치한다 (OQ-2 결정 (a) 경로, 2026-06-01). 응답 본문 schema 검증 + grep. + +## 측정 방식 및 데이터 출처 +각 성공 기준을 어떻게 측정할지와 어떤 데이터나 문서를 근거로 쓸지 적는다. + +- S-1 측정 방식: `grep -rn "ledger_trades\..*asset_id\|.asset_id\b" app/repositories/postgres.py app/domain/models.py app/api/v1_portfolios.py` 결과 0건 + DB `\d ledger_trades`에서 `portfolio_id` 컬럼 확인 / 데이터 출처: 코드 grep + alembic upgrade 후 DB schema +- S-2 측정 방식: pytest TestClient `/auth/login` body에 `no_expiry: true` 포함 → HTTP 422 응답 회귀 / 데이터 출처: 신규 pytest 케이스 (예: `tests/test_auth.py::test_login_rejects_no_expiry_extra_field`) +- S-3 측정 방식: pytest TestClient `GET /health` 응답 JSON에 `"db_enabled": true` 매치 (PORTFOLIOHUB_DB_ENABLED=true 환경) + 로컬 smoke `curl http://127.0.0.1:18000/health` / 데이터 출처: 신규 pytest 케이스 (예: `tests/test_app.py::test_health_exposes_db_enabled`) + 로컬 smoke + +# 8. REQ로 넘기기 전 확인 체크 +REQ 초안 작성 단계로 넘기기 전에 무엇이 확정되어야 하는지 확인한다. 미완료 항목은 후속 결정이 필요함을 뜻한다. + +- [x] 범위(In Scope/Out of Scope) 확정 + - 기준: OQ-1/OQ-2/OQ-3 모두 2026-06-01 Answered. 범위 + 배포 시점(REL-002 분리) 확정. +- [x] 성공 지표의 측정 방식/데이터 출처 확정 + - 기준: S-1(코드 grep + DB \d), S-2(pytest 422 회귀), S-3(pytest TestClient + 로컬 smoke) 모두 확정. +- [x] High 영향 리스크 우선순위 및 해소 책임자 지정 + - 기준: 본 Discovery에는 High 리스크 없음 (R-1/R-2/R-3 모두 Low) — 해소 책임자 지정 불요. +- [x] 모든 오픈 질문(OQ) `Open` 상태 없음 확인 + - 기준: OQ-1/OQ-2/OQ-3 모두 Answered (2026-06-01). Open 항목 0개. +- [x] Freeze 확정 담당자(CONFIRMED_BY) 지정 + - 기준: CONFIRMED_BY = "Justin Ko" (confirm-discovery 2026-06-01). + +# 9. 파일 처리 결과 +이 Discovery 문서 생성/갱신 결과와 참조 문서를 기록한다. 이후 추적과 리뷰 시 근거로 사용한다. + +- 처리 결과: 생성 +- 생성 경로: docs/discovery/dcy-005_20260601_rel-001-feedback-cleanup.md +- 참조 문서: + - [docs/releases/rel-001_20260521_v1-api-full-surface.md](../releases/rel-001_20260521_v1-api-full-surface.md) Feedback Handoff Discovery Input 항목 + - [docs/srs/Documentation/req-021_token-expiry-and-rotation-policy.md](../srs/Documentation/req-021_token-expiry-and-rotation-policy.md) (REQ-021 정책 본문) + - [docs/srs/Interface/req-029_v1-portfolios-ledger-trades-migration.md](../srs/Interface/req-029_v1-portfolios-ledger-trades-migration.md) Notes (ledger_trades 컬럼명 follow-up 출처) + - [app/api/health.py:17-32](../../app/api/health.py#L17-L32) HealthResponse 모델/handler + - [app/repositories/postgres.py:1284-1294](../../app/repositories/postgres.py#L1284-L1294) ledger_trades INSERT + - [app/services/auth_service.py:47-65](../../app/services/auth_service.py#L47-L65) AuthService.login + +# 10. 사용자 결정 필요 항목 요약 +사용자나 승인자가 결정해야 하는 항목만 모아 정리한다. 결정, 확인, 데이터 요청을 구분해서 적는다. + +## DECIDE +- (none — OQ-3 결정 완료, DECIDE 섹션 비움) + +## CONFIRM +- (none — 두 항목 모두 사용자 confirm-discovery(2026-06-01)로 가정 A-1/A-3에 대한 동의 확정. 본문 가정 섹션에 반영됨.) + +## DATA +- (none — 본 Discovery는 본 repo 내부 정보만으로 자립) + +# 11. Discovery Freeze +REQ 초안 작성 인계 시점의 확정 플래그와 섹션 참조만 기록한다. 내용 재작성 없이 원본 섹션을 링크로 대신한다. + +## 섹션 참조 +- 범위: [# 5. 요구사항 목록 > 범위 경계](#5-요구사항-목록) +- 리스크/오픈 질문: [# 6. 리스크/가정 목록](#6-리스크가정-목록) +- 성공 기준: [# 7. 초기 성공 기준](#7-초기-성공-기준) +- 인계 전 확인: [# 8. REQ로 넘기기 전 확인 체크](#8-req로-넘기기-전-확인-체크) +- 사용자 결정 필요: [# 10. 사용자 결정 필요 항목 요약](#10-사용자-결정-필요-항목-요약) + +## Freeze 플래그 +- Handoff Decision: 승인 +- Handoff Rationale: OQ-1/OQ-2/OQ-3 모두 Answered, CONFIRM 2건은 사용자 confirm-discovery(2026-06-01)로 가정 A-1/A-3 동의 확정. # 8 체크리스트 5건 모두 [x]. REQ Drafting 진입 준비 완료. +- Ready for REQ Drafting: true +- Confirmed By: Justin +- Confirmed At (KST): 2026-06-01 18:30 + +# 12. 작성 가이드 +- 이 파일은 dcy-005 작성 완료본이다. confirm-discovery 진입 전까지 OQ 결정 + 본문 반영 필요. +- 3건이 서로 독립이므로 OQ 결정도 항목별로 진행 가능. 결정이 끝나는 항목부터 본문(FR-2/FR-3)을 (a)/(b) 선택안으로 압축한다. +- 본 Discovery는 코드/문서/마이그레이션 모두 영향이 있는 표준 SDLC 트랙. minor-change fast path는 3건 묶음 특성상 부적합 — 표준 batch로 진행 권장. diff --git a/docs/discovery/index.md b/docs/discovery/index.md index f028589..939a899 100644 --- a/docs/discovery/index.md +++ b/docs/discovery/index.md @@ -10,3 +10,4 @@ | dcy-002_20260519_tag-ownership-and-applicability-policy | 20260519 | 태그 소유권 및 적용 대상 정책 — admin/user 구분 태그 모델과 asset·portfolio 적용 정책 도입 | [Discovery](./dcy-002_20260519_tag-ownership-and-applicability-policy.md) | confirmed | | dcy-003_20260520_user-portfolio-mutation-v1-api-migration | 20260520 | User Portfolio Mutation v1 API 이전 및 path 명명 분리 (/v1/portfolios* 신설) | [Discovery](./dcy-003_20260520_user-portfolio-mutation-v1-api-migration.md) | confirmed | | dcy-004_20260520_token-issuance-and-lifecycle-overhaul | 20260520 | 토큰 발급 및 관리 전면 정비 — client token 폐지, service-token rotate 정상화, 발급 권한 매트릭스, 만료·회전·자동 revoke 정책 | [Discovery](./dcy-004_20260520_token-issuance-and-lifecycle-overhaul.md) | confirmed | +| dcy-005_20260601_rel-001-feedback-cleanup | 20260601 | REL-001 Feedback 정합화 — ledger_trades.asset_id 명명 / session no_expiry enforcement / health db_enabled | [Discovery](./dcy-005_20260601_rel-001-feedback-cleanup.md) | confirmed | diff --git a/docs/interface-contract.md b/docs/interface-contract.md index ecd09a1..404230e 100644 --- a/docs/interface-contract.md +++ b/docs/interface-contract.md @@ -45,7 +45,7 @@ - JSON response models :: responses are shaped by app/schemas/auth.py, app/schemas/user.py, and app/schemas/portfolio.py - HTTP 201, 200, and 204 status codes :: create, read or update, and delete semantics respectively -- GET /health payload :: returns `{"status": "ok", "service": "portfolio-hub", "version": "", "checked_at": ""}` for readiness checks +- GET /health payload :: returns `{"status": "ok", "service": "portfolio-hub", "version": "", "checked_at": "", "db_enabled": }` for readiness checks. `db_enabled` exposes the `PORTFOLIOHUB_DB_ENABLED` setting value (not an actual DB connectivity ping); see `docs/runtime-flows.md` for the readiness semantics (REQ-035, dcy-005). ### Error Contract @@ -379,11 +379,12 @@ - Response: HTTP 204 (no content) - Error: HTTP 403 if service/client or ownership mismatch; HTTP 404 if portfolio or comment not found; HTTP 409 if disabled portfolio -### Endpoint: `POST /v1/portfolios/{portfolio_id}/ledger/trades` (REQ-029, BAT-011) +### Endpoint: `POST /v1/portfolios/{portfolio_id}/ledger/trades` (REQ-029, BAT-011; REQ-033 column rename BAT-015) - Auth: DB service token with `trade-write` permission; `daemon_id IS NULL` → HTTP 403 fail-closed - Request: `LedgerTradeRequest` — `order_id`, `execution_id`, `side` (buy|sell), `quantity`, `price`, `fee_amount`, `tax_amount`, `executed_at` - Response: HTTP 201 + `LedgerTradeResponse` with `status="new"`; HTTP 200 + `status="duplicate"` for idempotent replay; HTTP 409 for conflicting payload +- `LedgerTradeResponse` fields: `ledger_entry_id`, `portfolio_id` (renamed from `asset_id` by REQ-033 / dcy-005 — schema and DB column now match in meaning), `order_id`, `execution_id`, `side`, `quantity`, `price`, `fee_amount`, `tax_amount`, `executed_at`, `recorded_at`, `status` - Error: HTTP 403 (no daemon_id on token, portfolio unassigned, daemon mismatch); HTTP 404 (non-existent portfolio); HTTP 409 (disabled portfolio or payload conflict) - Idempotency key: `(portfolio_id, order_id, execution_id)` — reuses `uq_ledger_trades_idempotency` constraint - **구 path `POST /v1/assets/{asset_id}/ledger/trades`**: BAT-011에서 제거됨. 호출 시 HTTP 404. diff --git a/docs/runtime-flows.md b/docs/runtime-flows.md index 1156eed..0c0e404 100644 --- a/docs/runtime-flows.md +++ b/docs/runtime-flows.md @@ -94,6 +94,7 @@ - Permission levels read, trade-write, and admin must map consistently across API guards, issued tokens, and CLI mutations. - Request validation and auth resolution should happen before persistence mutation or response rendering. - The health endpoint remains unauthenticated so infrastructure can probe readiness without a token. +- `GET /health` response includes `db_enabled: bool` (REQ-035, dcy-005). 본 필드는 환경변수 `PORTFOLIOHUB_DB_ENABLED` 설정값을 그대로 노출하는 것이며 actual DB connectivity ping이 아니다. production-grade readiness probe (SELECT 1 ping 등)는 본 endpoint 범위 밖이며 별도 `/readiness` endpoint 분리는 후속 Discovery에서 다룬다. ## Flow: `v1 API — Tag Management (POST/GET/PATCH /v1/tags)` diff --git a/docs/srs/Interface/req-034_login-request-extra-forbid-enforcement.md b/docs/srs/Interface/req-034_login-request-extra-forbid-enforcement.md new file mode 100644 index 0000000..0007b9d --- /dev/null +++ b/docs/srs/Interface/req-034_login-request-extra-forbid-enforcement.md @@ -0,0 +1,126 @@ +# REQ-034: LoginRequest 추가 필드 거부 enforcement (session no_expiry 정책 정합) + +- Status: Implemented +- Type: Interface +- Priority: Medium +- Owner: Justin Ko + +## Intent +- REQ-021 정책 본문이 "session에서 `no_expiry` 시도 → HTTP 422 또는 ValueError 거부"로 명시하지만, 현재 `LoginRequest` 스키마([app/schemas/auth.py:13-23](../../app/schemas/auth.py#L13-L23))가 Pydantic 기본(`extra="ignore"`)이라 외부에서 `no_expiry: true`를 추가해 보내도 silently 무시된다. +- `LoginRequest`에 `extra="forbid"` 적용으로 정책 명문과 코드 동작을 일치시켜 audit 명확성을 확보한다. + +## Requirement +- `app/schemas/auth.py:LoginRequest`에 `model_config = ConfigDict(extra="forbid")`를 적용한다. +- 외부 client가 LoginRequest에 정의되지 않은 필드(`no_expiry` 포함, 기타 임의 필드)를 전송하면 FastAPI/Pydantic이 HTTP 422로 거부한다. +- `AuthService.login`([app/services/auth_service.py:47-65](../../app/services/auth_service.py#L47-L65))의 동작은 변경하지 않는다(정책 enforcement는 스키마 단계에서 처리). +- 기존 정상 client(username/password 2필드만 전송)는 영향 없음을 회귀 테스트로 확인한다. + +## Acceptance Criteria +1. Given valid username/password, When `POST /auth/login` body `{"username": "...", "password": "..."}` 호출, Then HTTP 200 + 정상 TokenResponse 반환(기존 동작 회귀). +2. Given valid username/password + 추가 필드, When `POST /auth/login` body `{"username": "...", "password": "...", "no_expiry": true}` 호출, Then HTTP 422 응답 + Pydantic validation error detail에 `extra_forbidden` 표기. +3. Given valid username/password + 임의 추가 필드, When `POST /auth/login` body `{"username": "...", "password": "...", "arbitrary_field": "x"}` 호출, Then HTTP 422 응답 (extra="forbid" 일반 동작 회귀). +4. Given codebase grep `grep -n "extra.*forbid" app/schemas/auth.py`, When 본 REQ 구현 후, Then `LoginRequest` 정의 부근에 매치된다. +5. Given 본 REQ 구현 후 전체 pytest 회귀, When 실행, Then 기존 통과 케이스 모두 유지. + +## Impacted Area +- Docs: + - (선택) `docs/srs/Documentation/req-021_token-expiry-and-rotation-policy.md` Notes에 "AC-4 enforcement는 REQ-034로 분리 구현" 추가 (cross-reference) +- Modules: + - `app/schemas/auth.py` — `LoginRequest.model_config = ConfigDict(extra="forbid")` 추가 +- Tests: + - 신규 `tests/test_auth.py::test_login_rejects_no_expiry_extra_field` — AC-2 검증 + - 신규 `tests/test_auth.py::test_login_rejects_arbitrary_extra_field` — AC-3 검증 (또는 AC-2와 단일 케이스로 합침) + +## Notes +- 본 REQ는 dcy-005 FR-2 산출물. REQ-021 follow-up. +- `extra="forbid"`는 `LoginRequest` 전체에 추가 필드 거부 효과. 기존 정상 client는 username/password만 전송하므로 영향 없음. 본 워크스페이스는 개발용이라 외부 monitoring 영향 무. +- 본 REQ는 별도 REL-002로 분리 배포(dcy-005 OQ-3 결정, 2026-06-01). +- Source: dcy-005 FR-2, REQ-021 AC-5 follow-up. + +## Change Log + +### CHG-20260601-03 +- Date: 2026-06-01 +- Author: Justin Ko + +#### Change Summary +- Status: Approved → Implemented + +#### Intent +- BAT-015 confirm-batch-verification 완료(2026-06-05). AC-1~5 모두 PASSED. `LoginRequest` extra="forbid" 적용 + pytest 422 회귀 2건 신규 + 로컬 smoke 422 + extra_forbidden 확인. + +#### Acceptance Criteria Delta +- No change + +#### Impacted Area Delta +- No change + +#### Delivery Trace Delta +- Batches: + - BAT-015 (release-candidate) +- Releases: + - REL-002 (예정) + +#### Revalidation Impact +- Reverification Needed: no +- Existing Implementation Invalidated: no +- Status Recommendation: implemented +- Follow-up Action: none + +### CHG-20260601-02 +- Date: 2026-06-01 +- Author: Justin Ko + +#### Change Summary +- Status: Proposed → Approved + +#### Intent +- 승인 게이트 통과: 필수 필드(Status/Type/Priority/Owner/Intent/Requirement/AC/Impacted Area/Change Log) 모두 충족, AC-1~5 구체성 확인 완료, blocker 없음. + +#### Acceptance Criteria Delta +- No change + +#### Impacted Area Delta +- No change + +#### Delivery Trace Delta +- Batches: none (미정) +- Releases: none (미정 — REL-002 분리 예정) + +#### Revalidation Impact +- Reverification Needed: no +- Existing Implementation Invalidated: no +- Status Recommendation: promote-to-approved +- Follow-up Action: batch 계획 편입 (BAT-015 후보) + +### CHG-20260601-01 +- Date: 2026-06-01 +- Author: Justin Ko + +#### Change Summary +- 최초 생성 — dcy-005 confirm-discovery 후 draft-req 실행. dcy-005 FR-2 산출물. + +#### Intent +- REQ-021 정책 본문과 LoginRequest 코드 동작 일치 (session no_expiry 거부 enforcement). + +#### Acceptance Criteria Delta +- Added: + - AC-1~5 최초 작성 + +#### Impacted Area Delta +- Docs: (선택) docs/srs/Documentation/req-021_*.md cross-reference — 최초 지정 +- Modules: app/schemas/auth.py — 최초 지정 +- Tests: tests/test_auth.py — 최초 지정 + +#### Delivery Trace Delta +- Batches: none (미정) +- Releases: none (미정 — REL-002 분리 예정) + +#### Revalidation Impact +- Reverification Needed: no +- Existing Implementation Invalidated: no +- Status Recommendation: keep-current +- Follow-up Action: confirm-req 후 batch 계획 편입 + +#### Validation Plan +- pytest TestClient로 AC-1~3 검증; grep으로 AC-4 검증 diff --git a/docs/srs/Interface/req-035_health-response-db-enabled-field.md b/docs/srs/Interface/req-035_health-response-db-enabled-field.md new file mode 100644 index 0000000..e145256 --- /dev/null +++ b/docs/srs/Interface/req-035_health-response-db-enabled-field.md @@ -0,0 +1,128 @@ +# REQ-035: GET /health 응답에 db_enabled 필드 추가 + +- Status: Implemented +- Type: Interface +- Priority: Medium +- Owner: Justin Ko + +## Intent +- 현재 `HealthResponse` 모델([app/api/health.py:17-21](../../app/api/health.py#L17-L21))이 `status`, `service`, `version`, `checked_at` 4필드만 노출하여 외부 health probe가 DB 사용 설정 상태를 직접 관찰할 수 없다. +- REL-001 verification 체크리스트의 "GET /health → HTTP 200 + `db_enabled: true`" 요구사항과 응답 모델 gap을 닫는다. +- 본 필드는 환경변수 `PORTFOLIOHUB_DB_ENABLED` 설정값을 노출하는 것이며 actual DB connectivity ping이 아님을 baseline 문서(`docs/runtime-flows.md`)에 명문화한다. + +## Requirement +- `HealthResponse` 모델에 `db_enabled: bool` 필드를 추가한다. +- `healthcheck()` 핸들러가 `settings.db_enabled`(환경변수 `PORTFOLIOHUB_DB_ENABLED` 설정값)를 `db_enabled` 필드에 채워서 응답한다. +- `docs/runtime-flows.md`에 "GET /health 응답의 `db_enabled`는 환경변수 설정값 노출이며 actual DB connectivity ping이 아니다. production-grade readiness probe는 별도 `/readiness` endpoint 분리로 후속 처리한다"는 의미 명문화 노트를 추가한다. +- `docs/interface-contract.md`에 /health 응답 schema의 `db_enabled` 필드 명세를 추가한다. + +## Acceptance Criteria +1. Given PORTFOLIOHUB_DB_ENABLED=true 환경, When `GET /health` 호출, Then HTTP 200 응답 본문에 `"db_enabled": true` 필드가 포함된다. +2. Given PORTFOLIOHUB_DB_ENABLED=false 환경, When `GET /health` 호출, Then HTTP 200 응답 본문에 `"db_enabled": false` 필드가 포함된다. +3. Given 응답 본문, When `status`, `service`, `version`, `checked_at`, `db_enabled` 5필드 확인, Then 모두 노출되고 기존 4필드 동작은 회귀 통과한다. +4. Given grep `grep -n "db_enabled" docs/runtime-flows.md`, When 실행, Then "설정값 노출, actual ping 아님" 의미 노트가 매치된다. +5. Given grep `grep -n "/health.*db_enabled\|db_enabled.*health" docs/interface-contract.md`, When 실행, Then /health 응답 schema에 `db_enabled` 필드 명세가 매치된다. + +## Impacted Area +- Docs: + - `docs/runtime-flows.md` — /health probe flow 섹션에 "db_enabled는 설정값 노출, actual ping 아님" 의미 노트 추가 (baseline 문서 갱신) + - `docs/interface-contract.md` — /health 응답 schema에 `db_enabled: bool` 필드 추가 +- Modules: + - `app/api/health.py` — `HealthResponse`에 `db_enabled: bool` 필드 추가 + `healthcheck()` 핸들러가 `settings.db_enabled` 채움 +- Tests: + - 신규 `tests/test_app.py::test_health_exposes_db_enabled` — AC-1 검증 (PORTFOLIOHUB_DB_ENABLED=true 환경) + - (선택) 추가 케이스 — AC-2 검증 (PORTFOLIOHUB_DB_ENABLED=false 환경) + +## Notes +- 본 REQ는 dcy-005 FR-3 산출물. REL-001 verification gap을 닫는 정합화. +- `db_enabled` 값은 단순 설정값 노출이며 actual DB connectivity 보장이 아니다. production-grade readiness probe(SELECT 1 ping 등)는 본 REQ 범위 밖이며 별도 후속 Discovery + `/readiness` endpoint 분리로 처리. +- 본 REQ는 별도 REL-002로 분리 배포(dcy-005 OQ-3 결정, 2026-06-01). REL-001은 현 상태(체크리스트 `db_enabled` 미노출 [~] 부분 통과)로 배포 진행. +- Source: dcy-005 FR-3, rel-001 Feedback Handoff 후보. + +## Change Log + +### CHG-20260601-03 +- Date: 2026-06-01 +- Author: Justin Ko + +#### Change Summary +- Status: Approved → Implemented + +#### Intent +- BAT-015 confirm-batch-verification 완료(2026-06-05). AC-1~5 모두 PASSED. HealthResponse 확장 + handler + pytest 3건(test_healthcheck 갱신 + 신규 2건) + baseline 문서(`docs/runtime-flows.md`) 명문화 + interface-contract.md 정합 모두 evidence 확보. + +#### Acceptance Criteria Delta +- No change + +#### Impacted Area Delta +- No change + +#### Delivery Trace Delta +- Batches: + - BAT-015 (release-candidate) +- Releases: + - REL-002 (예정) + +#### Revalidation Impact +- Reverification Needed: no +- Existing Implementation Invalidated: no +- Status Recommendation: implemented +- Follow-up Action: 미래 production-grade readiness probe(`/readiness` endpoint 분리)는 본 REQ 범위 밖, 별도 후속 Discovery 후보. + +### CHG-20260601-02 +- Date: 2026-06-01 +- Author: Justin Ko + +#### Change Summary +- Status: Proposed → Approved + +#### Intent +- 승인 게이트 통과: 필수 필드(Status/Type/Priority/Owner/Intent/Requirement/AC/Impacted Area/Change Log) 모두 충족, AC-1~5 구체성 확인 완료, blocker 없음. baseline 문서(docs/runtime-flows.md) 갱신 요구가 AC-4에 반영됨. + +#### Acceptance Criteria Delta +- No change + +#### Impacted Area Delta +- No change + +#### Delivery Trace Delta +- Batches: none (미정) +- Releases: none (미정 — REL-002 분리 예정) + +#### Revalidation Impact +- Reverification Needed: no +- Existing Implementation Invalidated: no +- Status Recommendation: promote-to-approved +- Follow-up Action: batch 계획 편입 (BAT-015 후보) + +### CHG-20260601-01 +- Date: 2026-06-01 +- Author: Justin Ko + +#### Change Summary +- 최초 생성 — dcy-005 confirm-discovery 후 draft-req 실행. dcy-005 FR-3 산출물. + +#### Intent +- REL-001 verification checklist의 `/health db_enabled` gap을 닫고 응답 모델 확장 + baseline 문서(runtime-flows.md) 명문화. + +#### Acceptance Criteria Delta +- Added: + - AC-1~5 최초 작성 + +#### Impacted Area Delta +- Docs: docs/runtime-flows.md (baseline), docs/interface-contract.md — 최초 지정 +- Modules: app/api/health.py — 최초 지정 +- Tests: tests/test_app.py — 최초 지정 + +#### Delivery Trace Delta +- Batches: none (미정) +- Releases: none (미정 — REL-002 분리 예정) + +#### Revalidation Impact +- Reverification Needed: no +- Existing Implementation Invalidated: no +- Status Recommendation: keep-current +- Follow-up Action: confirm-req 후 batch 계획 편입 + +#### Validation Plan +- pytest TestClient로 AC-1~3 검증; grep으로 AC-4/AC-5 baseline 문서 명문화 검증 diff --git a/docs/srs/Migration/req-033_ledger-trades-asset-id-to-portfolio-id-rename.md b/docs/srs/Migration/req-033_ledger-trades-asset-id-to-portfolio-id-rename.md new file mode 100644 index 0000000..aced67a --- /dev/null +++ b/docs/srs/Migration/req-033_ledger-trades-asset-id-to-portfolio-id-rename.md @@ -0,0 +1,132 @@ +# REQ-033: ledger_trades.asset_id → portfolio_id 컬럼명 정합화 + +- Status: Implemented +- Type: Migration +- Priority: Medium +- Owner: Justin Ko + +## Intent +- `ledger_trades` 테이블의 `asset_id` 컬럼이 실제로는 portfolio_id를 저장하지만 컬럼명이 의미와 불일치하여 schema reader가 assets 테이블 참조로 오해할 수 있다. +- REQ-029 구현(POST /v1/portfolios/{id}/ledger/trades 이전) 시점의 잔여 명명 정합화를 마무리한다. + +## Requirement +- Alembic 신규 revision을 작성해 `ledger_trades.asset_id` 컬럼을 `portfolio_id`로 rename한다(`ALTER TABLE ledger_trades RENAME COLUMN asset_id TO portfolio_id`). +- 기존 데이터는 동일 의미(portfolio_id 저장)이므로 backfill 불요. +- `app/repositories/postgres.py`의 ledger_trades INSERT/SELECT/RETURNING SQL에서 컬럼명을 `portfolio_id`로 갱신한다. +- `app/domain/models.py`의 `LedgerTrade` dataclass에 `asset_id` 필드가 있다면 `portfolio_id`로 rename한다(있는 경우). +- `app/api/v1_portfolios.py` ledger route handler에서 응답에 `asset_id` 노출이 있다면 `portfolio_id`로 정합화한다(있는 경우). +- 마이그레이션 lock 시간을 운영 영향 < 1초로 유지하기 위해 짧은 단일 `ALTER COLUMN RENAME` 트랜잭션으로 처리한다. + +## Acceptance Criteria +1. Given 신규 Alembic revision 적용 후, When `\d ledger_trades` 실행, Then `portfolio_id` 컬럼이 존재하고 `asset_id` 컬럼은 존재하지 않는다. +2. Given AC-1 환경, When 기존 `POST /v1/portfolios/{id}/ledger/trades` 호출, Then INSERT/SELECT 회귀 테스트가 모두 통과한다. +3. Given codebase grep `grep -rn "ledger_trades\..*asset_id\|asset_id.*ledger_trades" app/repositories/postgres.py app/domain/models.py app/api/v1_portfolios.py`, When 본 REQ 구현 후, Then 결과는 0건이다. +4. Given 마이그레이션 staging dry-run, When `ALTER COLUMN RENAME` 실행 시간 측정, Then 측정 시간 < 1초 (NFR-1 정합). +5. Given 본 REQ 구현 후 전체 pytest 회귀, When 실행, Then 기존 통과 케이스 모두 유지 + ledger_trades 의존 케이스 통과. + +## Impacted Area +- Docs: + - `docs/data-model.md` — ledger_trades 컬럼 명세에 `portfolio_id` 반영, `asset_id` 표기 제거 + - (선택) `docs/interface-contract.md` — ledger 응답에 `asset_id` 노출이 있다면 `portfolio_id`로 갱신 +- Modules: + - `migrations/versions/` — 신규 revision (예: `20260601_NNNN_ledger_trades_portfolio_id_rename.py`) + - `app/repositories/postgres.py` — ledger_trades INSERT/SELECT/RETURNING 컬럼명 갱신([app/repositories/postgres.py:1284-1314](../../app/repositories/postgres.py#L1284-L1314)) + - `app/domain/models.py` — `LedgerTrade` dataclass 필드명 (있다면) + - `app/api/v1_portfolios.py` — ledger route handler 응답 매핑 (있다면) +- Tests: + - 갱신 `tests/test_v1_portfolios_ledger.py` — 컬럼명 변경 회귀 검증 + - (선택) 신규 마이그레이션 dry-run 회귀 + +## Notes +- 본 REQ는 dcy-005 FR-1 산출물. 정합화 묶음 trigger. +- 외부 client(trex-basic 등)는 HTTP API(`POST /v1/portfolios/{id}/ledger/trades`)만 호출하므로 DB 컬럼명 의존 없음 추정. 마이그레이션 전 외부 query 의존성 확인. +- 본 REQ는 별도 REL-002로 분리 배포(dcy-005 OQ-3 결정, 2026-06-01). +- Source: dcy-005 FR-1, REQ-029 Notes follow-up. + +## Change Log + +### CHG-20260601-03 +- Date: 2026-06-01 +- Author: Justin Ko + +#### Change Summary +- Status: Approved → Implemented + +#### Intent +- BAT-015 confirm-batch-verification 완료(2026-06-05). AC-2/3/5 PASSED, AC-1/4 staging Deferred(blocker 아님). 마이그레이션 + 코드/스키마/도메인/라우트/테스트/문서 정합화 모두 evidence 확보. + +#### Acceptance Criteria Delta +- No change + +#### Impacted Area Delta +- No change + +#### Delivery Trace Delta +- Batches: + - BAT-015 (release-candidate) +- Releases: + - REL-002 (예정) + +#### Revalidation Impact +- Reverification Needed: no +- Existing Implementation Invalidated: no +- Status Recommendation: implemented +- Follow-up Action: REL-002 staging 시점에 AC-1 (`\d ledger_trades` 컬럼 검증) + AC-4 (마이그레이션 lock < 1초) evidence 수집. 본 워크스페이스 ledger_trades 테이블 미생성 상태 자체는 별도 후속 항목(BAT-011 mock verification 후속) 등록 후보. + +### CHG-20260601-02 +- Date: 2026-06-01 +- Author: Justin Ko + +#### Change Summary +- Status: Proposed → Approved + +#### Intent +- 승인 게이트 통과: 필수 필드(Status/Type/Priority/Owner/Intent/Requirement/AC/Impacted Area/Change Log) 모두 충족, AC-1~5 구체성 확인 완료, blocker 없음. + +#### Acceptance Criteria Delta +- No change + +#### Impacted Area Delta +- No change + +#### Delivery Trace Delta +- Batches: none (미정) +- Releases: none (미정 — REL-002 분리 예정) + +#### Revalidation Impact +- Reverification Needed: no +- Existing Implementation Invalidated: no +- Status Recommendation: promote-to-approved +- Follow-up Action: batch 계획 편입 (BAT-015 후보) + +### CHG-20260601-01 +- Date: 2026-06-01 +- Author: Justin Ko + +#### Change Summary +- 최초 생성 — dcy-005 confirm-discovery 후 draft-req 실행. dcy-005 FR-1 산출물. + +#### Intent +- ledger_trades.asset_id 컬럼명을 의미에 맞게 portfolio_id로 정합화. + +#### Acceptance Criteria Delta +- Added: + - AC-1~5 최초 작성 + +#### Impacted Area Delta +- Docs: docs/data-model.md, (선택) docs/interface-contract.md — 최초 지정 +- Modules: migrations/versions/, app/repositories/postgres.py, app/domain/models.py, app/api/v1_portfolios.py — 최초 지정 +- Tests: tests/test_v1_portfolios_ledger.py — 최초 지정 + +#### Delivery Trace Delta +- Batches: none (미정) +- Releases: none (미정 — REL-002 분리 예정) + +#### Revalidation Impact +- Reverification Needed: no +- Existing Implementation Invalidated: no +- Status Recommendation: keep-current +- Follow-up Action: confirm-req 후 batch 계획 편입; staging 환경에서 ALTER COLUMN RENAME dry-run으로 lock 시간 측정 + +#### Validation Plan +- pytest로 AC-1~5 검증 + staging dry-run AC-4 측정 diff --git a/docs/srs/index.md b/docs/srs/index.md index 7be6c39..16da5f1 100644 --- a/docs/srs/index.md +++ b/docs/srs/index.md @@ -74,7 +74,7 @@ ## Next Requirement ID -- Current: `REQ-033` +- Current: `REQ-036` - Rule: `Requirement Register`에 있는 가장 큰 REQ 번호보다 정확히 1 커야 한다. ## Requirement Register @@ -113,11 +113,14 @@ | REQ-030 | portfolio tag 적용/해제/조회 v1 라우트 | Interface | Implemented | Medium | Justin Ko | [req-030](./Interface/req-030_v1-portfolios-tags-crud.md) | | REQ-031 | portfolio comment CRUD v1 라우트 | Interface | Implemented | Medium | Justin Ko | [req-031](./Interface/req-031_v1-portfolios-comments-crud.md) | | REQ-032 | portfolio v1 baseline 문서 갱신 | Documentation | Implemented | Medium | Justin Ko | [req-032](./Documentation/req-032_portfolio-v1-baseline-docs-update.md) | +| REQ-033 | ledger_trades.asset_id → portfolio_id 컬럼명 정합화 | Migration | Implemented | Medium | Justin Ko | [req-033](./Migration/req-033_ledger-trades-asset-id-to-portfolio-id-rename.md) | +| REQ-034 | LoginRequest 추가 필드 거부 enforcement | Interface | Implemented | Medium | Justin Ko | [req-034](./Interface/req-034_login-request-extra-forbid-enforcement.md) | +| REQ-035 | GET /health 응답에 db_enabled 필드 추가 | Interface | Implemented | Medium | Justin Ko | [req-035](./Interface/req-035_health-response-db-enabled-field.md) | ## Recent Change Log Summary +- 2026-06-05: REQ-033/034/035 Approved → Implemented (CHG-20260601-03 각각) — BAT-015 confirm-batch-verification 완료(2026-06-05). 전체 372 passed + 로컬 smoke 5건 통과. REQ-033 AC-1/AC-4 실 DB evidence는 REL-002 staging 이연(blocker 아님). BAT-015 status: release-candidate. +- 2026-06-01: REQ-033/034/035 Proposed → Approved (CHG-20260601-02 각각) — confirm-req 무인자 일괄 모드 실행, 승인 게이트(필수 필드/AC 구체성/Change Log) 모두 충족, blocker 없음. dcy-005 산출물 3건 모두 Approved. +- 2026-06-01: REQ-033/034/035 신규 (Proposed) — dcy-005(rel-001-feedback-cleanup) draft-req 산출물. REQ-033 ledger_trades.asset_id → portfolio_id 컬럼명 정합화 (Migration), REQ-034 LoginRequest extra="forbid" enforcement (Interface, REQ-021 follow-up), REQ-035 HealthResponse db_enabled 필드 추가 + docs/runtime-flows.md baseline 갱신 (Interface). 3건 모두 REL-002 분리 배포 예정(dcy-005 OQ-3). - 2026-06-01: REQ-028 Approved → Implemented (CHG-20260601-02) — CHG-20260601-01 augmentation에 대한 verification evidence 확보. pytest 368 PASSED + 로컬 smoke로 GET /v1/portfolios 응답에 `display_name` 노출 확인. BAT-009 verification.md AC-13 매핑 누적. REL-001 실 배포 환경 smoke는 배포 시 최종 확인. - 2026-06-01: REQ-028 Implemented → Approved (CHG-20260601-01) — GET /v1/portfolios 응답 schema에 `display_name` 필드 추가 augmentation. portfolio가 참조하는 asset의 display_name을 join하여 UI/CLI 단일 호출 표시 가능. AC-13 신설, Portfolio domain model + PortfolioItem schema + repo join 보강. BAT-009/BAT-014 verification 재실행 필요. -- 2026-05-24: REQ-025 / REQ-026 / REQ-027 / REQ-028 / REQ-032 Approved → Implemented (CHG-20260524-02 각각) — BAT-014 verification 완료. `PortfolioItem.symbol` 응답 계약이 코드(`app/schemas/v1.py`, `app/api/v1_portfolios.py`), 테스트(`tests/test_v1_portfolios_crud.py`, `tests/test_v1_portfolios_list.py`), baseline 문서(`docs/interface-contract.md`)에 반영됨. BAT-014는 release-candidate로 승격되었고 BAT-009/BAT-010의 관련 response-contract evidence는 superseded. -- 2026-05-24: REQ-025 / REQ-026 / REQ-027 / REQ-028 / REQ-032 Implemented → Approved (CHG-20260524-01 각각) — trex-orbit 팀 이슈(`docs/issues/20260524_portfolio-hub-ticket-symbol-in-portfolios.md`) 후속. `PortfolioItem.symbol` 응답 계약 추가 및 `GET /v1/portfolios` 의 asset join 기반 응답(`portfolios JOIN assets` → `assets.symbol`)을 명문화. BAT-009 / BAT-010 및 REL-001의 기존 response-contract evidence는 부분 무효화되어 follow-up BAT-014에서 재검증 필요. -- 2026-05-21: REQ-005 Approved → Implemented (CHG-20260521-03) — BAT-013 confirm-batch-verification 완료. `append_asset_tag_id`/`remove_asset_tag_id` 메서드 신규 + 라우트 호출 교체 + 회귀 테스트 7건 추가. assets 테이블 mutation 정합화 evidence 확보. 전체 369 passed (BAT-012 baseline 362 → +7). BAT-004의 AC-1/AC-5 evidence는 BAT-013에서 superseded. CLI 동일 mismatch는 follow-up REQ 후보. diff --git a/migrations/versions/20260601_0024_ledger_trades_portfolio_id_rename.py b/migrations/versions/20260601_0024_ledger_trades_portfolio_id_rename.py new file mode 100644 index 0000000..08af60e --- /dev/null +++ b/migrations/versions/20260601_0024_ledger_trades_portfolio_id_rename.py @@ -0,0 +1,53 @@ +"""Rename ledger_trades.asset_id to portfolio_id for column-name semantic alignment (REQ-033) + +Revision ID: 20260601_0024 +Revises: 20260521_0023 +Create Date: 2026-06-01 00:00:00 + +Background: +- REQ-029 implementation stored `portfolio_id` values in the `asset_id` column of `ledger_trades`, + causing schema readers to misinterpret the column as an `assets.asset_id` reference. +- dcy-005 FR-1 + REQ-033 mandates renaming the column to align name and meaning. +- No backfill required: existing values are already `portfolio_id` semantics. +""" + +from __future__ import annotations + +from alembic import op +from sqlalchemy import inspect, text + +revision = "20260601_0024" +down_revision = "20260521_0023" +branch_labels = None +depends_on = None + + +def _column_exists(inspector, table_name: str, column_name: str) -> bool: + cols = inspector.get_columns(table_name) + return any(c["name"] == column_name for c in cols) + + +def upgrade() -> None: + bind = op.get_bind() + inspector = inspect(bind) + + if not inspector.has_table("ledger_trades"): + return + + if _column_exists(inspector, "ledger_trades", "asset_id") and not _column_exists( + inspector, "ledger_trades", "portfolio_id" + ): + op.execute(text("ALTER TABLE ledger_trades RENAME COLUMN asset_id TO portfolio_id")) + + +def downgrade() -> None: + bind = op.get_bind() + inspector = inspect(bind) + + if not inspector.has_table("ledger_trades"): + return + + if _column_exists(inspector, "ledger_trades", "portfolio_id") and not _column_exists( + inspector, "ledger_trades", "asset_id" + ): + op.execute(text("ALTER TABLE ledger_trades RENAME COLUMN portfolio_id TO asset_id")) diff --git a/tests/test_app.py b/tests/test_app.py index 1f0924d..3af6fac 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -18,6 +18,31 @@ def test_healthcheck() -> None: assert body["service"] == "portfolio-hub" assert "version" in body assert "checked_at" in body + assert "db_enabled" in body + assert isinstance(body["db_enabled"], bool) + + +def test_health_exposes_db_enabled_true(monkeypatch) -> None: + """REQ-035 AC-1: PORTFOLIOHUB_DB_ENABLED=true → db_enabled: true.""" + monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "true") + monkeypatch.setenv("CREDENTIAL_ENCRYPTION_KEY", "test-key-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") + get_settings.cache_clear() + client = TestClient(create_app()) + response = client.get("/health") + get_settings.cache_clear() + assert response.status_code == 200 + assert response.json()["db_enabled"] is True + + +def test_health_exposes_db_enabled_false(monkeypatch) -> None: + """REQ-035 AC-2: PORTFOLIOHUB_DB_ENABLED=false → db_enabled: false.""" + monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "false") + get_settings.cache_clear() + client = TestClient(create_app()) + response = client.get("/health") + get_settings.cache_clear() + assert response.status_code == 200 + assert response.json()["db_enabled"] is False def test_docs_are_exposed() -> None: diff --git a/tests/test_auth.py b/tests/test_auth.py index decc8a1..c56fa6c 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -283,3 +283,35 @@ def test_post_auth_tokens_returns_404(monkeypatch) -> None: _app.dependency_overrides.clear() assert response.status_code in (404, 405) + + +# REQ-034: LoginRequest extra="forbid" enforcement (dcy-005 FR-2) + +def test_login_rejects_no_expiry_extra_field(monkeypatch) -> None: + """REQ-034 AC-2: session 발급 경로에 no_expiry 시도 → HTTP 422 (extra="forbid").""" + monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "false") + get_settings.cache_clear() + client = TestClient(app) + + resp = client.post( + "/auth/login", + json={"username": "anyone", "password": "any", "no_expiry": True}, + ) + + assert resp.status_code == 422 + body = resp.json() + assert any("extra" in (err.get("type") or "") for err in body.get("detail", [])) + + +def test_login_rejects_arbitrary_extra_field(monkeypatch) -> None: + """REQ-034 AC-3: 임의 추가 필드 → HTTP 422 (extra="forbid" 일반 동작).""" + monkeypatch.setenv("PORTFOLIOHUB_DB_ENABLED", "false") + get_settings.cache_clear() + client = TestClient(app) + + resp = client.post( + "/auth/login", + json={"username": "anyone", "password": "any", "arbitrary_field": "x"}, + ) + + assert resp.status_code == 422 diff --git a/tests/test_daemon_assignment.py b/tests/test_daemon_assignment.py index 83dd1ee..dd9c8d9 100644 --- a/tests/test_daemon_assignment.py +++ b/tests/test_daemon_assignment.py @@ -213,7 +213,7 @@ def test_ledger_trade_201_when_daemon_matches(monkeypatch) -> None: portfolio = _portfolio_with_daemon(daemon_id="d-1") trade = MagicMock() trade.ledger_entry_id = 1 - trade.asset_id = "asset-1" + trade.portfolio_id = "asset-1" trade.order_id = "ord-1" trade.execution_id = "exec-1" trade.side = "buy" diff --git a/tests/test_v1.py b/tests/test_v1.py index 01db00a..32ac81e 100644 --- a/tests/test_v1.py +++ b/tests/test_v1.py @@ -359,7 +359,7 @@ def _trade_payload(**overrides) -> dict: def _make_ledger_trade(**kwargs) -> LedgerTrade: defaults = dict( ledger_entry_id=1, - asset_id="005930", + portfolio_id="005930", order_id="ord-001", execution_id="exec-001", side="buy", @@ -411,7 +411,7 @@ def test_v1_ledger_trade_created(monkeypatch) -> None: assert resp.status_code == 201 body = resp.json() assert body["status"] == "new" - assert body["asset_id"] == "005930" + assert body["portfolio_id"] == "005930" def test_v1_ledger_trade_idempotent_same_payload(monkeypatch) -> None: diff --git a/tests/test_v1_portfolios_ledger.py b/tests/test_v1_portfolios_ledger.py index 3631ca4..7fbc338 100644 --- a/tests/test_v1_portfolios_ledger.py +++ b/tests/test_v1_portfolios_ledger.py @@ -58,7 +58,7 @@ def _make_portfolio( def _make_trade(**kwargs) -> LedgerTrade: defaults = dict( ledger_entry_id=1, - asset_id=_PORTFOLIO_ID, + portfolio_id=_PORTFOLIO_ID, order_id="ord-001", execution_id="exec-001", side="buy", @@ -130,7 +130,7 @@ def test_ledger_trade_new() -> None: assert resp.status_code == 201 body = resp.json() assert body["status"] == "new" - assert body["asset_id"] == _PORTFOLIO_ID + assert body["portfolio_id"] == _PORTFOLIO_ID # ---------------------------------------------------------------------------