phase 14-15: docker build validation and helm deployment
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user