"""감사 P0 수정 검증: 인증 누락 / 레거시 엔진 병존 / 혼재 종목 재태깅.""" import inspect from decimal import Decimal from unittest.mock import AsyncMock, patch import pytest from app.api.endpoints import day_trading as dt from app.api.endpoints.portfolio import portfolio_cache from app.models.models import Portfolio from app.services import portfolio_cache_bridge as bridge def _deps(fn_name: str): """해당 라우트에 걸린 의존성 이름 목록 (중첩 포함).""" for r in dt.router.routes: if getattr(r, "endpoint", None) is not None and r.endpoint.__name__ == fn_name: names, stack = [], list(r.dependant.dependencies) while stack: d = stack.pop() if d.call is not None: names.append(getattr(d.call, "__name__", str(d.call))) stack.extend(d.dependencies) return names raise AssertionError(f"route not found: {fn_name}") @pytest.mark.parametrize("fn", ["set_watchlist", "execute_day_trade", "close_all_positions", "manual_cycle"]) def test_write_endpoints_require_api_key(fn): """CLAUDE.md §4: 쓰기 엔드포인트는 require_api_key 필수 (인증 없이 전량청산 가능했음).""" assert "require_api_key" in _deps(fn), f"{fn}: 인증 누락" def test_legacy_execute_blocks_real_orders(): src = inspect.getsource(dt.execute_day_trade) assert "if not req.dry_run:" in src and "410" in src def test_close_all_delegates_to_engine(): """레거시 인메모리로 청산하면 엔진이 같은 물량을 30초 뒤 재매도한다.""" src = inspect.getsource(dt.close_all_positions) assert "force_close_all" in src and "_execute_eod_flat" not in src def test_startup_does_not_restore_legacy_positions(): import app.main as m src = inspect.getsource(m) assert "restore_positions_from_db()" not in src, "레거시 복원이 남아 있으면 이중 매도" @pytest.mark.asyncio async def test_dt_buy_does_not_retag_existing_swing_row(): """스윙 보유 종목을 DT가 추가 매수해도 행 전략 태깅을 바꾸면 안 된다. 바꾸면 get_swing_holdings 에서 빠져 스윙 보유분의 손절·익절이 완전히 멈춘다. """ portfolio_cache.holdings = [Portfolio( stock_code="064400", stock_name="LG씨엔에스", quantity=8, average_price=Decimal("70148"), current_price=Decimal("70800"), strategy_type="swing", )] portfolio_cache.summary = {"available_cash": 1_000_000, "total_value": 0, "total_investment": 0, "total_assets": 1_000_000, "total_profit_loss": 0, "total_profit_loss_rate": 0, "total_stocks": 1} q = AsyncMock() with patch("app.services.db_worker.enqueue_portfolio_upsert", q): await bridge.reflect_buy("064400", "LG씨엔에스", 4, 70800.0, strategy_type="daytrading") assert portfolio_cache.holdings[0].strategy_type == "swing" assert portfolio_cache.holdings[0].quantity == 12 assert "strategy_type" not in q.await_args.args[0], "기존 행에 전략 태깅을 덮어쓰면 안 됨" @pytest.mark.asyncio async def test_new_row_keeps_strategy_tag(): portfolio_cache.holdings = [] portfolio_cache.summary = None q = AsyncMock() with patch("app.services.db_worker.enqueue_portfolio_upsert", q): await bridge.reflect_buy("005930", "삼성전자", 3, 70000.0, strategy_type="daytrading") assert q.await_args.args[0]["strategy_type"] == "daytrading" assert portfolio_cache.holdings[0].strategy_type == "daytrading"