"""Tests for the operator approval workflow for live trading mode. Validates: - requires_approval logic for paper/live/disabled modes - ApprovalRequest model behavior (pending, expired) - compute_expiry calculation - Integration with broker service process_order_job flow """ from datetime import datetime, timedelta, timezone from services.risk.approval import ( ApprovalRequest, ApprovalStatus, compute_expiry, requires_approval, ) from services.risk.engine import ( OperatorApproval, PortfolioRiskConfig, TradingMode, ) # --------------------------------------------------------------------------- # requires_approval tests # --------------------------------------------------------------------------- class TestRequiresApproval: def test_paper_mode_auto_approved(self): """Paper orders are auto-approved by default.""" config = PortfolioRiskConfig(trading_mode=TradingMode.PAPER) assert requires_approval(config) is False def test_paper_mode_approval_required_when_auto_approve_off(self): """Paper orders need approval when auto_approve_paper is False.""" config = PortfolioRiskConfig( trading_mode=TradingMode.PAPER, operator_approval=OperatorApproval(auto_approve_paper=False), ) assert requires_approval(config) is True def test_live_mode_requires_approval_by_default(self): """Live orders require approval by default.""" config = PortfolioRiskConfig(trading_mode=TradingMode.LIVE) assert requires_approval(config) is True def test_live_mode_no_approval_when_disabled(self): """Live orders skip approval when require_approval_for_live is False.""" config = PortfolioRiskConfig( trading_mode=TradingMode.LIVE, operator_approval=OperatorApproval(require_approval_for_live=False), ) assert requires_approval(config) is False def test_disabled_mode_never_requires_approval(self): """Disabled mode never requires approval (blocked upstream).""" config = PortfolioRiskConfig(trading_mode=TradingMode.DISABLED) assert requires_approval(config) is False def test_override_trading_mode_parameter(self): """The trading_mode parameter overrides config.trading_mode.""" config = PortfolioRiskConfig(trading_mode=TradingMode.PAPER) # Override to live — should require approval assert requires_approval(config, trading_mode=TradingMode.LIVE) is True # --------------------------------------------------------------------------- # compute_expiry tests # --------------------------------------------------------------------------- class TestComputeExpiry: def test_default_30_minutes(self): now = datetime(2026, 4, 11, 12, 0, 0, tzinfo=timezone.utc) config = PortfolioRiskConfig() expiry = compute_expiry(config, now=now) assert expiry == now + timedelta(minutes=30) def test_custom_timeout(self): now = datetime(2026, 4, 11, 12, 0, 0, tzinfo=timezone.utc) config = PortfolioRiskConfig( operator_approval=OperatorApproval(approval_timeout_minutes=60), ) expiry = compute_expiry(config, now=now) assert expiry == now + timedelta(minutes=60) # --------------------------------------------------------------------------- # ApprovalRequest model tests # --------------------------------------------------------------------------- class TestApprovalRequest: def test_defaults(self): req = ApprovalRequest(ticker="AAPL") assert req.ticker == "AAPL" assert req.status == ApprovalStatus.PENDING assert req.is_pending is True assert req.approval_id # auto-generated UUID def test_is_expired_when_past_expiry(self): past = datetime.now(timezone.utc) - timedelta(minutes=5) req = ApprovalRequest(ticker="AAPL", expires_at=past) assert req.is_expired is True def test_not_expired_when_future_expiry(self): future = datetime.now(timezone.utc) + timedelta(minutes=30) req = ApprovalRequest(ticker="AAPL", expires_at=future) assert req.is_expired is False def test_approved_is_not_expired(self): past = datetime.now(timezone.utc) - timedelta(minutes=5) req = ApprovalRequest( ticker="AAPL", status=ApprovalStatus.APPROVED, expires_at=past, ) assert req.is_expired is False def test_to_dict_roundtrip(self): req = ApprovalRequest( ticker="MSFT", side="sell", quantity=50.0, estimated_value=15000.0, recommendation_id="rec-123", ) d = req.to_dict() assert d["ticker"] == "MSFT" assert d["side"] == "sell" assert d["quantity"] == 50.0 assert d["status"] == "pending" assert d["recommendation_id"] == "rec-123" def test_explicit_expired_status(self): req = ApprovalRequest( ticker="AAPL", status=ApprovalStatus.EXPIRED, ) assert req.is_expired is True assert req.is_pending is False