1018 lines
35 KiB
Python
1018 lines
35 KiB
Python
"""Unit tests for Settings model (Issue #5: DB-4).
|
|
|
|
Tests for Settings model fields including:
|
|
- RiskProfile enum (CONSERVATIVE, MODERATE, AGGRESSIVE)
|
|
- risk_score (0-10 validation)
|
|
- max_position_pct (0-100 validation)
|
|
- max_portfolio_risk_pct (0-100 validation)
|
|
- investment_horizon_years (>=0 validation)
|
|
- alert_preferences (JSON structure)
|
|
- One-to-one relationship with User
|
|
- Unique constraint on user_id
|
|
- Cascade delete behavior
|
|
- CheckConstraints for numeric bounds
|
|
|
|
Follows TDD principles with comprehensive coverage.
|
|
Tests written BEFORE implementation (RED phase).
|
|
"""
|
|
|
|
import pytest
|
|
import json
|
|
from decimal import Decimal
|
|
from sqlalchemy import select
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
# Mark all tests in this module as asyncio
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
class TestSettingsBasicFields:
|
|
"""Tests for basic Settings model fields."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_settings_with_required_fields(self, db_session, test_user):
|
|
"""Should create settings with only required fields (user_id)."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings, RiskProfile
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
# Assert
|
|
assert settings.id is not None
|
|
assert settings.user_id == test_user.id
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_settings_defaults(self, db_session, test_user):
|
|
"""Should apply default values to optional fields."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings, RiskProfile
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
# Check defaults
|
|
assert settings.risk_profile == RiskProfile.MODERATE
|
|
assert settings.risk_score == Decimal("5.0")
|
|
assert settings.max_position_pct == Decimal("10.0")
|
|
assert settings.max_portfolio_risk_pct == Decimal("2.0")
|
|
assert settings.investment_horizon_years == 5
|
|
assert settings.alert_preferences == {}
|
|
assert settings.created_at is not None
|
|
assert settings.updated_at is not None
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_settings_with_all_fields(self, db_session, test_user):
|
|
"""Should create settings with all fields specified."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings, RiskProfile
|
|
|
|
alert_prefs = {
|
|
"email": {
|
|
"enabled": True,
|
|
"address": "user@example.com",
|
|
"alert_types": ["price_alert", "portfolio_alert"]
|
|
},
|
|
"sms": {
|
|
"enabled": True,
|
|
"phone": "+1234567890",
|
|
"rate_limit": {"max_per_hour": 5}
|
|
}
|
|
}
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
risk_profile=RiskProfile.AGGRESSIVE,
|
|
risk_score=Decimal("8.5"),
|
|
max_position_pct=Decimal("25.0"),
|
|
max_portfolio_risk_pct=Decimal("5.0"),
|
|
investment_horizon_years=10,
|
|
alert_preferences=alert_prefs,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
# Assert all fields
|
|
assert settings.id is not None
|
|
assert settings.user_id == test_user.id
|
|
assert settings.risk_profile == RiskProfile.AGGRESSIVE
|
|
assert settings.risk_score == Decimal("8.5")
|
|
assert settings.max_position_pct == Decimal("25.0")
|
|
assert settings.max_portfolio_risk_pct == Decimal("5.0")
|
|
assert settings.investment_horizon_years == 10
|
|
assert settings.alert_preferences == alert_prefs
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_settings_timestamps_auto_populate(self, db_session, test_user):
|
|
"""Should auto-populate created_at and updated_at timestamps."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
from datetime import datetime
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
# Assert timestamps exist and are recent
|
|
assert settings.created_at is not None
|
|
assert settings.updated_at is not None
|
|
assert isinstance(settings.created_at, datetime)
|
|
assert isinstance(settings.updated_at, datetime)
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestRiskProfileEnum:
|
|
"""Tests for RiskProfile enum validation."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_risk_profile_conservative(self, db_session, test_user):
|
|
"""Should create settings with CONSERVATIVE risk profile."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings, RiskProfile
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
risk_profile=RiskProfile.CONSERVATIVE,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.risk_profile == RiskProfile.CONSERVATIVE
|
|
assert settings.risk_profile.value == "CONSERVATIVE"
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_risk_profile_moderate(self, db_session, test_user):
|
|
"""Should create settings with MODERATE risk profile."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings, RiskProfile
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
risk_profile=RiskProfile.MODERATE,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.risk_profile == RiskProfile.MODERATE
|
|
assert settings.risk_profile.value == "MODERATE"
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_risk_profile_aggressive(self, db_session, test_user):
|
|
"""Should create settings with AGGRESSIVE risk profile."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings, RiskProfile
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
risk_profile=RiskProfile.AGGRESSIVE,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.risk_profile == RiskProfile.AGGRESSIVE
|
|
assert settings.risk_profile.value == "AGGRESSIVE"
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_risk_profile_invalid_value(self, db_session, test_user):
|
|
"""Should reject invalid risk profile values."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
# Attempting to use invalid value should raise ValueError
|
|
with pytest.raises((ValueError, AttributeError)):
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
risk_profile="INVALID_RISK_PROFILE",
|
|
)
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestRiskScoreValidation:
|
|
"""Tests for risk_score field validation (0-10 range)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_risk_score_minimum_valid(self, db_session, test_user):
|
|
"""Should accept risk_score of 0 (minimum valid)."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
risk_score=Decimal("0.0"),
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.risk_score == Decimal("0.0")
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_risk_score_maximum_valid(self, db_session, test_user):
|
|
"""Should accept risk_score of 10 (maximum valid)."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
risk_score=Decimal("10.0"),
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.risk_score == Decimal("10.0")
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_risk_score_mid_range(self, db_session, test_user):
|
|
"""Should accept mid-range risk_score values."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
risk_score=Decimal("5.5"),
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.risk_score == Decimal("5.5")
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_risk_score_out_of_range(self, db_session, test_user):
|
|
"""Should reject risk_score outside 0-10 range."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
# Store user_id before async operations to avoid lazy load after rollback
|
|
user_id = test_user.id
|
|
|
|
# Test negative value
|
|
settings = Settings(
|
|
user_id=user_id,
|
|
risk_score=Decimal("-1.0"),
|
|
)
|
|
db_session.add(settings)
|
|
|
|
with pytest.raises(IntegrityError):
|
|
await db_session.commit()
|
|
|
|
await db_session.rollback()
|
|
|
|
# Test value > 10
|
|
settings2 = Settings(
|
|
user_id=user_id,
|
|
risk_score=Decimal("11.0"),
|
|
)
|
|
db_session.add(settings2)
|
|
|
|
with pytest.raises(IntegrityError):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestMaxPositionPctValidation:
|
|
"""Tests for max_position_pct field validation (0-100 range)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_max_position_pct_minimum_valid(self, db_session, test_user):
|
|
"""Should accept max_position_pct of 0."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
max_position_pct=Decimal("0.0"),
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.max_position_pct == Decimal("0.0")
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_max_position_pct_maximum_valid(self, db_session, test_user):
|
|
"""Should accept max_position_pct of 100."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
max_position_pct=Decimal("100.0"),
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.max_position_pct == Decimal("100.0")
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_max_position_pct_out_of_range(self, db_session, test_user):
|
|
"""Should reject max_position_pct outside 0-100 range."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
# Store user_id before async operations to avoid lazy load after rollback
|
|
user_id = test_user.id
|
|
|
|
# Test negative value
|
|
settings = Settings(
|
|
user_id=user_id,
|
|
max_position_pct=Decimal("-1.0"),
|
|
)
|
|
db_session.add(settings)
|
|
|
|
with pytest.raises(IntegrityError):
|
|
await db_session.commit()
|
|
|
|
await db_session.rollback()
|
|
|
|
# Test value > 100
|
|
settings2 = Settings(
|
|
user_id=user_id,
|
|
max_position_pct=Decimal("101.0"),
|
|
)
|
|
db_session.add(settings2)
|
|
|
|
with pytest.raises(IntegrityError):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestMaxPortfolioRiskPctValidation:
|
|
"""Tests for max_portfolio_risk_pct field validation (0-100 range)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_max_portfolio_risk_pct_minimum_valid(self, db_session, test_user):
|
|
"""Should accept max_portfolio_risk_pct of 0."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
max_portfolio_risk_pct=Decimal("0.0"),
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.max_portfolio_risk_pct == Decimal("0.0")
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_max_portfolio_risk_pct_maximum_valid(self, db_session, test_user):
|
|
"""Should accept max_portfolio_risk_pct of 100."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
max_portfolio_risk_pct=Decimal("100.0"),
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.max_portfolio_risk_pct == Decimal("100.0")
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_max_portfolio_risk_pct_out_of_range(self, db_session, test_user):
|
|
"""Should reject max_portfolio_risk_pct outside 0-100 range."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
# Store user_id before async operations to avoid lazy load after rollback
|
|
user_id = test_user.id
|
|
|
|
# Test negative value
|
|
settings = Settings(
|
|
user_id=user_id,
|
|
max_portfolio_risk_pct=Decimal("-1.0"),
|
|
)
|
|
db_session.add(settings)
|
|
|
|
with pytest.raises(IntegrityError):
|
|
await db_session.commit()
|
|
|
|
await db_session.rollback()
|
|
|
|
# Test value > 100
|
|
settings2 = Settings(
|
|
user_id=user_id,
|
|
max_portfolio_risk_pct=Decimal("101.0"),
|
|
)
|
|
db_session.add(settings2)
|
|
|
|
with pytest.raises(IntegrityError):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestInvestmentHorizonValidation:
|
|
"""Tests for investment_horizon_years field validation (>=0)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_investment_horizon_valid_positive(self, db_session, test_user):
|
|
"""Should accept positive investment horizon values."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
investment_horizon_years=15,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.investment_horizon_years == 15
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_investment_horizon_zero_valid(self, db_session, test_user):
|
|
"""Should accept investment horizon of 0."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
investment_horizon_years=0,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.investment_horizon_years == 0
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_investment_horizon_negative_invalid(self, db_session, test_user):
|
|
"""Should reject negative investment horizon values."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
investment_horizon_years=-1,
|
|
)
|
|
db_session.add(settings)
|
|
|
|
with pytest.raises(IntegrityError):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestAlertPreferencesJSON:
|
|
"""Tests for alert_preferences JSON field."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alert_preferences_empty_dict(self, db_session, test_user):
|
|
"""Should accept empty dict as alert preferences."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
alert_preferences={},
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.alert_preferences == {}
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alert_preferences_email_config(self, db_session, test_user):
|
|
"""Should store email alert preferences."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
email_prefs = {
|
|
"email": {
|
|
"enabled": True,
|
|
"address": "user@example.com",
|
|
"alert_types": ["price_alert", "portfolio_alert", "execution_alert"]
|
|
}
|
|
}
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
alert_preferences=email_prefs,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.alert_preferences == email_prefs
|
|
assert settings.alert_preferences["email"]["enabled"] is True
|
|
assert settings.alert_preferences["email"]["address"] == "user@example.com"
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alert_preferences_sms_config(self, db_session, test_user):
|
|
"""Should store SMS alert preferences."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
sms_prefs = {
|
|
"sms": {
|
|
"enabled": True,
|
|
"phone": "+1234567890",
|
|
"alert_types": ["critical_alert"],
|
|
"rate_limit": {"max_per_hour": 5, "max_per_day": 20}
|
|
}
|
|
}
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
alert_preferences=sms_prefs,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.alert_preferences == sms_prefs
|
|
assert settings.alert_preferences["sms"]["phone"] == "+1234567890"
|
|
assert settings.alert_preferences["sms"]["rate_limit"]["max_per_hour"] == 5
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alert_preferences_multiple_channels(self, db_session, test_user):
|
|
"""Should store preferences for multiple alert channels."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
multi_channel_prefs = {
|
|
"email": {
|
|
"enabled": True,
|
|
"address": "user@example.com",
|
|
"alert_types": ["price_alert", "portfolio_alert"]
|
|
},
|
|
"sms": {
|
|
"enabled": True,
|
|
"phone": "+1234567890",
|
|
"rate_limit": {"max_per_hour": 5}
|
|
},
|
|
"push": {
|
|
"enabled": False,
|
|
"device_tokens": []
|
|
}
|
|
}
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
alert_preferences=multi_channel_prefs,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.alert_preferences == multi_channel_prefs
|
|
assert "email" in settings.alert_preferences
|
|
assert "sms" in settings.alert_preferences
|
|
assert "push" in settings.alert_preferences
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alert_preferences_nested_structure(self, db_session, test_user):
|
|
"""Should handle deeply nested JSON structures."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
nested_prefs = {
|
|
"email": {
|
|
"enabled": True,
|
|
"address": "user@example.com",
|
|
"filters": {
|
|
"price_alerts": {
|
|
"min_change_pct": 5.0,
|
|
"symbols": ["AAPL", "GOOGL", "MSFT"]
|
|
},
|
|
"portfolio_alerts": {
|
|
"thresholds": {
|
|
"daily_loss": -2.0,
|
|
"daily_gain": 5.0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
alert_preferences=nested_prefs,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.alert_preferences == nested_prefs
|
|
assert settings.alert_preferences["email"]["filters"]["price_alerts"]["min_change_pct"] == 5.0
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alert_preferences_rate_limiting(self, db_session, test_user):
|
|
"""Should store rate limiting configuration."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
rate_limit_prefs = {
|
|
"sms": {
|
|
"enabled": True,
|
|
"phone": "+1234567890",
|
|
"rate_limit": {
|
|
"max_per_hour": 5,
|
|
"max_per_day": 20,
|
|
"max_per_week": 100,
|
|
"cooldown_minutes": 15
|
|
}
|
|
}
|
|
}
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
alert_preferences=rate_limit_prefs,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.alert_preferences["sms"]["rate_limit"]["max_per_hour"] == 5
|
|
assert settings.alert_preferences["sms"]["rate_limit"]["max_per_day"] == 20
|
|
assert settings.alert_preferences["sms"]["rate_limit"]["cooldown_minutes"] == 15
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alert_preferences_update(self, db_session, test_user):
|
|
"""Should allow updating alert preferences."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
alert_preferences={"email": {"enabled": False}},
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
# Update preferences
|
|
settings.alert_preferences = {
|
|
"email": {"enabled": True, "address": "new@example.com"}
|
|
}
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.alert_preferences["email"]["enabled"] is True
|
|
assert settings.alert_preferences["email"]["address"] == "new@example.com"
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_alert_preferences_null_allowed(self, db_session, test_user):
|
|
"""Should allow NULL alert preferences if column is nullable."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
alert_preferences=None,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
# Should default to empty dict or be None
|
|
assert settings.alert_preferences is None or settings.alert_preferences == {}
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestUserRelationship:
|
|
"""Tests for Settings-User one-to-one relationship."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_settings_belongs_to_user(self, db_session, test_user):
|
|
"""Should establish relationship between settings and user."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
# Verify relationship
|
|
assert settings.user_id == test_user.id
|
|
# If relationship is set up, we should be able to access user
|
|
# assert settings.user == test_user # Uncomment when relationship is implemented
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_one_settings_per_user_constraint(self, db_session, test_user):
|
|
"""Should enforce unique constraint - one settings per user."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
# Create first settings
|
|
settings1 = Settings(user_id=test_user.id)
|
|
db_session.add(settings1)
|
|
await db_session.commit()
|
|
|
|
# Try to create second settings for same user
|
|
settings2 = Settings(user_id=test_user.id)
|
|
db_session.add(settings2)
|
|
|
|
with pytest.raises(IntegrityError):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_settings_cascade_delete_with_user(self, db_session, test_user):
|
|
"""Should cascade delete settings when user is deleted."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
from tradingagents.api.models import User
|
|
|
|
# Create settings
|
|
settings = Settings(user_id=test_user.id)
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
|
|
settings_id = settings.id
|
|
|
|
# Delete user
|
|
await db_session.delete(test_user)
|
|
await db_session.commit()
|
|
|
|
# Verify settings was deleted
|
|
result = await db_session.execute(
|
|
select(Settings).where(Settings.id == settings_id)
|
|
)
|
|
deleted_settings = result.scalar_one_or_none()
|
|
|
|
assert deleted_settings is None
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_users_can_have_settings(self, db_session, test_user, second_user):
|
|
"""Should allow multiple users to have their own settings."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings, RiskProfile
|
|
|
|
# Create settings for first user
|
|
settings1 = Settings(
|
|
user_id=test_user.id,
|
|
risk_profile=RiskProfile.CONSERVATIVE,
|
|
)
|
|
db_session.add(settings1)
|
|
|
|
# Create settings for second user
|
|
settings2 = Settings(
|
|
user_id=second_user.id,
|
|
risk_profile=RiskProfile.AGGRESSIVE,
|
|
)
|
|
db_session.add(settings2)
|
|
|
|
await db_session.commit()
|
|
await db_session.refresh(settings1)
|
|
await db_session.refresh(settings2)
|
|
|
|
# Verify both settings exist with different configurations
|
|
assert settings1.user_id == test_user.id
|
|
assert settings1.risk_profile == RiskProfile.CONSERVATIVE
|
|
assert settings2.user_id == second_user.id
|
|
assert settings2.risk_profile == RiskProfile.AGGRESSIVE
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestSettingsConstraints:
|
|
"""Tests for database constraints and edge cases."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_risk_score_boundary_values(self, db_session, test_user):
|
|
"""Should accept exact boundary values for risk_score."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
# Test 0 exactly
|
|
settings1 = Settings(user_id=test_user.id, risk_score=Decimal("0"))
|
|
db_session.add(settings1)
|
|
await db_session.commit()
|
|
assert settings1.risk_score == Decimal("0")
|
|
|
|
await db_session.delete(settings1)
|
|
await db_session.commit()
|
|
|
|
# Test 10 exactly (with new user since one-to-one)
|
|
from tradingagents.api.models import User
|
|
from tradingagents.api.services.auth_service import hash_password
|
|
|
|
user2 = User(
|
|
username="testuser2",
|
|
email="test2@example.com",
|
|
hashed_password=hash_password("password123"),
|
|
)
|
|
db_session.add(user2)
|
|
await db_session.commit()
|
|
await db_session.refresh(user2)
|
|
|
|
settings2 = Settings(user_id=user2.id, risk_score=Decimal("10"))
|
|
db_session.add(settings2)
|
|
await db_session.commit()
|
|
assert settings2.risk_score == Decimal("10")
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_percentage_boundary_values(self, db_session, test_user):
|
|
"""Should accept exact boundary values for percentage fields."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
max_position_pct=Decimal("0"),
|
|
max_portfolio_risk_pct=Decimal("100"),
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.max_position_pct == Decimal("0")
|
|
assert settings.max_portfolio_risk_pct == Decimal("100")
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_decimal_precision_preserved(self, db_session, test_user):
|
|
"""Should preserve decimal precision for numeric fields."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
|
|
settings = Settings(
|
|
user_id=test_user.id,
|
|
risk_score=Decimal("7.5"),
|
|
max_position_pct=Decimal("15.25"),
|
|
max_portfolio_risk_pct=Decimal("3.75"),
|
|
)
|
|
|
|
db_session.add(settings)
|
|
await db_session.commit()
|
|
await db_session.refresh(settings)
|
|
|
|
assert settings.risk_score == Decimal("7.5")
|
|
assert settings.max_position_pct == Decimal("15.25")
|
|
assert settings.max_portfolio_risk_pct == Decimal("3.75")
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_required_user_id_constraint(self, db_session):
|
|
"""Should require user_id (NOT NULL constraint)."""
|
|
try:
|
|
from tradingagents.api.models.settings import Settings
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
settings = Settings(
|
|
risk_profile="MODERATE",
|
|
# user_id intentionally missing
|
|
)
|
|
db_session.add(settings)
|
|
|
|
with pytest.raises(IntegrityError):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Settings model not yet implemented (TDD RED phase)")
|