959 lines
34 KiB
Python
959 lines
34 KiB
Python
"""Unit tests for Portfolio model (Issue #4: DB-3).
|
|
|
|
Tests for Portfolio model fields including:
|
|
- portfolio_type (LIVE, PAPER, BACKTEST enum)
|
|
- initial_capital, current_value (Decimal precision)
|
|
- currency (3-letter code validation)
|
|
- Unique constraint on (user_id, name)
|
|
- Cascade delete behavior
|
|
- Decimal(19,4) precision for monetary values
|
|
|
|
Follows TDD principles with comprehensive coverage.
|
|
Tests written BEFORE implementation (RED phase).
|
|
"""
|
|
|
|
import pytest
|
|
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 TestPortfolioModelBasicFields:
|
|
"""Tests for basic Portfolio model fields."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_portfolio_with_required_fields(self, db_session, test_user):
|
|
"""Should create portfolio with only required fields."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="My First Portfolio",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
# Assert
|
|
assert portfolio.id is not None
|
|
assert portfolio.user_id == test_user.id
|
|
assert portfolio.name == "My First Portfolio"
|
|
assert portfolio.portfolio_type == PortfolioType.PAPER
|
|
assert portfolio.initial_capital == Decimal("10000.0000")
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_portfolio_defaults(self, db_session, test_user):
|
|
"""Should apply default values to optional fields."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Test Portfolio",
|
|
portfolio_type=PortfolioType.LIVE,
|
|
initial_capital=Decimal("50000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
# Check defaults
|
|
assert portfolio.current_value == portfolio.initial_capital
|
|
assert portfolio.currency == "AUD"
|
|
assert portfolio.is_active is True
|
|
assert portfolio.created_at is not None
|
|
assert portfolio.updated_at is not None
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_portfolio_with_all_fields(self, db_session, test_user):
|
|
"""Should create portfolio with all fields specified."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Complete Portfolio",
|
|
portfolio_type=PortfolioType.BACKTEST,
|
|
initial_capital=Decimal("100000.5000"),
|
|
current_value=Decimal("105000.7500"),
|
|
currency="USD",
|
|
is_active=False,
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
# Assert all fields
|
|
assert portfolio.id is not None
|
|
assert portfolio.user_id == test_user.id
|
|
assert portfolio.name == "Complete Portfolio"
|
|
assert portfolio.portfolio_type == PortfolioType.BACKTEST
|
|
assert portfolio.initial_capital == Decimal("100000.5000")
|
|
assert portfolio.current_value == Decimal("105000.7500")
|
|
assert portfolio.currency == "USD"
|
|
assert portfolio.is_active is False
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_portfolio_timestamps_auto_populate(self, db_session, test_user):
|
|
"""Should auto-populate created_at and updated_at timestamps."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
from datetime import datetime
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Timestamp Test",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
# Assert timestamps exist and are recent
|
|
assert portfolio.created_at is not None
|
|
assert portfolio.updated_at is not None
|
|
assert isinstance(portfolio.created_at, datetime)
|
|
assert isinstance(portfolio.updated_at, datetime)
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestPortfolioTypeEnum:
|
|
"""Tests for PortfolioType enum validation."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_portfolio_type_live(self, db_session, test_user):
|
|
"""Should create portfolio with LIVE type."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Live Portfolio",
|
|
portfolio_type=PortfolioType.LIVE,
|
|
initial_capital=Decimal("50000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
assert portfolio.portfolio_type == PortfolioType.LIVE
|
|
assert portfolio.portfolio_type.value == "LIVE"
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_portfolio_type_paper(self, db_session, test_user):
|
|
"""Should create portfolio with PAPER type."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Paper Portfolio",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
assert portfolio.portfolio_type == PortfolioType.PAPER
|
|
assert portfolio.portfolio_type.value == "PAPER"
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_portfolio_type_backtest(self, db_session, test_user):
|
|
"""Should create portfolio with BACKTEST type."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Backtest Portfolio",
|
|
portfolio_type=PortfolioType.BACKTEST,
|
|
initial_capital=Decimal("100000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
assert portfolio.portfolio_type == PortfolioType.BACKTEST
|
|
assert portfolio.portfolio_type.value == "BACKTEST"
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_portfolio_type_invalid_value(self, db_session, test_user):
|
|
"""Should reject invalid portfolio type."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
# Try to create with invalid string
|
|
with pytest.raises((ValueError, AttributeError)):
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Invalid Portfolio",
|
|
portfolio_type="INVALID_TYPE",
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestPortfolioDecimalPrecision:
|
|
"""Tests for Decimal(19,4) precision on monetary values."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_initial_capital_decimal_precision(self, db_session, test_user):
|
|
"""Should store initial_capital with 4 decimal places."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Precision Test",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("12345.6789"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
# Assert decimal precision maintained
|
|
assert portfolio.initial_capital == Decimal("12345.6789")
|
|
assert isinstance(portfolio.initial_capital, Decimal)
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_current_value_decimal_precision(self, db_session, test_user):
|
|
"""Should store current_value with 4 decimal places."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Value Test",
|
|
portfolio_type=PortfolioType.LIVE,
|
|
initial_capital=Decimal("10000.0000"),
|
|
current_value=Decimal("10523.4567"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
# Assert decimal precision maintained
|
|
assert portfolio.current_value == Decimal("10523.4567")
|
|
assert isinstance(portfolio.current_value, Decimal)
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_large_capital_value(self, db_session, test_user):
|
|
"""Should handle large capital values (up to 15 digits before decimal)."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
# Test with 14 digits before decimal point (SQLite has limited precision)
|
|
# PostgreSQL can handle 15 digits, but SQLite rounds at ~15 significant figures
|
|
large_value = Decimal("99999999999999.9999")
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Large Portfolio",
|
|
portfolio_type=PortfolioType.LIVE,
|
|
initial_capital=large_value,
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
# Allow small precision loss for SQLite compatibility
|
|
assert abs(portfolio.initial_capital - large_value) < Decimal("1.0")
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_small_capital_value(self, db_session, test_user):
|
|
"""Should handle small capital values with precision."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
small_value = Decimal("0.0001")
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Small Portfolio",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=small_value,
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
assert portfolio.initial_capital == small_value
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_negative_values_rejected(self, db_session, test_user):
|
|
"""Should reject negative capital values (business rule)."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Negative Test",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("-1000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
|
|
# Should raise constraint violation or validation error
|
|
with pytest.raises((IntegrityError, ValueError)):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestPortfolioUniqueConstraint:
|
|
"""Tests for unique constraint on (user_id, name)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_can_have_multiple_portfolios(self, db_session, test_user):
|
|
"""Should allow user to create multiple portfolios with different names."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio1 = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Portfolio 1",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
portfolio2 = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Portfolio 2",
|
|
portfolio_type=PortfolioType.LIVE,
|
|
initial_capital=Decimal("50000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio1)
|
|
db_session.add(portfolio2)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio1)
|
|
await db_session.refresh(portfolio2)
|
|
|
|
assert portfolio1.id != portfolio2.id
|
|
assert portfolio1.user_id == portfolio2.user_id
|
|
assert portfolio1.name != portfolio2.name
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_duplicate_name_same_user_rejected(self, db_session, test_user):
|
|
"""Should reject duplicate portfolio name for same user."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio1 = Portfolio(
|
|
user_id=test_user.id,
|
|
name="My Portfolio",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio1)
|
|
await db_session.commit()
|
|
|
|
# Try to create another portfolio with same name for same user
|
|
portfolio2 = Portfolio(
|
|
user_id=test_user.id,
|
|
name="My Portfolio",
|
|
portfolio_type=PortfolioType.LIVE,
|
|
initial_capital=Decimal("20000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio2)
|
|
|
|
with pytest.raises(IntegrityError):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_same_name_different_users_allowed(self, db_session, test_user, another_user):
|
|
"""Should allow different users to have portfolios with same name."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio1 = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Main Portfolio",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
portfolio2 = Portfolio(
|
|
user_id=another_user.id,
|
|
name="Main Portfolio",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("15000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio1)
|
|
db_session.add(portfolio2)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio1)
|
|
await db_session.refresh(portfolio2)
|
|
|
|
assert portfolio1.id != portfolio2.id
|
|
assert portfolio1.name == portfolio2.name
|
|
assert portfolio1.user_id != portfolio2.user_id
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestPortfolioCurrencyValidation:
|
|
"""Tests for currency field validation."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_default_currency_aud(self, db_session, test_user):
|
|
"""Should default to AUD currency."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Default Currency",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
assert portfolio.currency == "AUD"
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_common_currencies(self, db_session, test_user):
|
|
"""Should accept common 3-letter currency codes."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
currencies = ["USD", "EUR", "GBP", "JPY", "CNY", "AUD", "CAD"]
|
|
|
|
for i, currency in enumerate(currencies):
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name=f"Portfolio {currency}",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
currency=currency,
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
assert portfolio.currency == currency
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_currency_uppercase_enforced(self, db_session, test_user):
|
|
"""Should store currency in uppercase."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Currency Case Test",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
currency="usd", # lowercase input
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
# Should be stored in uppercase
|
|
assert portfolio.currency == "USD"
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_invalid_currency_length(self, db_session, test_user):
|
|
"""Should reject currency codes that aren't 3 characters."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
# Test with 2-character code
|
|
with pytest.raises((ValueError, IntegrityError)):
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Invalid Currency 1",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
currency="US",
|
|
)
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestPortfolioRelationships:
|
|
"""Tests for Portfolio relationships with User."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_portfolio_belongs_to_user(self, db_session, test_user):
|
|
"""Should establish relationship from portfolio to user."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="User Portfolio",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
# Load the relationship
|
|
await db_session.refresh(portfolio, ["user"])
|
|
|
|
assert portfolio.user is not None
|
|
assert portfolio.user.id == test_user.id
|
|
assert portfolio.user.username == test_user.username
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_has_many_portfolios(self, db_session, test_user):
|
|
"""Should establish relationship from user to portfolios."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio1 = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Portfolio A",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
portfolio2 = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Portfolio B",
|
|
portfolio_type=PortfolioType.LIVE,
|
|
initial_capital=Decimal("50000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio1)
|
|
db_session.add(portfolio2)
|
|
await db_session.commit()
|
|
|
|
# Refresh user with portfolios relationship
|
|
await db_session.refresh(test_user, ["portfolios"])
|
|
|
|
assert len(test_user.portfolios) == 2
|
|
portfolio_names = [p.name for p in test_user.portfolios]
|
|
assert "Portfolio A" in portfolio_names
|
|
assert "Portfolio B" in portfolio_names
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cascade_delete_when_user_deleted(self, db_session, test_user):
|
|
"""Should delete portfolios when user is deleted (cascade)."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Will Be Deleted",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
portfolio_id = portfolio.id
|
|
|
|
# Delete the user
|
|
await db_session.delete(test_user)
|
|
await db_session.commit()
|
|
|
|
# Check portfolio is also deleted
|
|
result = await db_session.execute(
|
|
select(Portfolio).where(Portfolio.id == portfolio_id)
|
|
)
|
|
deleted_portfolio = result.scalar_one_or_none()
|
|
|
|
assert deleted_portfolio is None
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestPortfolioEdgeCases:
|
|
"""Tests for edge cases and boundary conditions."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_very_long_portfolio_name(self, db_session, test_user):
|
|
"""Should handle long portfolio names within limits."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
long_name = "A" * 255 # Assume 255 char limit
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name=long_name,
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
assert portfolio.name == long_name
|
|
assert len(portfolio.name) == 255
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_portfolio_name_too_long(self, db_session, test_user):
|
|
"""Should reject portfolio names exceeding max length."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
too_long_name = "A" * 256 # Exceed 255 char limit
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name=too_long_name,
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
|
|
with pytest.raises((ValueError, IntegrityError)):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_unicode_in_portfolio_name(self, db_session, test_user):
|
|
"""Should handle unicode characters in portfolio name."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
unicode_name = "我的投资组合 🚀 Portfolio Émigré"
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name=unicode_name,
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
assert portfolio.name == unicode_name
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_empty_portfolio_name(self, db_session, test_user):
|
|
"""Should reject empty portfolio name."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
|
|
with pytest.raises((ValueError, IntegrityError)):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_zero_initial_capital(self, db_session, test_user):
|
|
"""Should handle zero initial capital."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Zero Capital",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("0.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
assert portfolio.initial_capital == Decimal("0.0000")
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_portfolio_repr(self, db_session, test_user):
|
|
"""Should have meaningful string representation."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Repr Test",
|
|
portfolio_type=PortfolioType.LIVE,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
await db_session.refresh(portfolio)
|
|
|
|
repr_str = repr(portfolio)
|
|
assert "Portfolio" in repr_str
|
|
assert str(portfolio.id) in repr_str
|
|
assert portfolio.name in repr_str
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestPortfolioQueryOperations:
|
|
"""Tests for querying Portfolio records."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_query_portfolio_by_id(self, db_session, test_user):
|
|
"""Should retrieve portfolio by ID."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Query Test",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
portfolio_id = portfolio.id
|
|
|
|
# Query by ID
|
|
result = await db_session.execute(
|
|
select(Portfolio).where(Portfolio.id == portfolio_id)
|
|
)
|
|
found = result.scalar_one_or_none()
|
|
|
|
assert found is not None
|
|
assert found.id == portfolio_id
|
|
assert found.name == "Query Test"
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_query_portfolios_by_user(self, db_session, test_user, another_user):
|
|
"""Should retrieve all portfolios for a user."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
# Create portfolios for test_user
|
|
for i in range(3):
|
|
portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name=f"User1 Portfolio {i}",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
db_session.add(portfolio)
|
|
|
|
# Create portfolio for another_user
|
|
portfolio = Portfolio(
|
|
user_id=another_user.id,
|
|
name="User2 Portfolio",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("15000.0000"),
|
|
)
|
|
db_session.add(portfolio)
|
|
await db_session.commit()
|
|
|
|
# Query portfolios for test_user
|
|
result = await db_session.execute(
|
|
select(Portfolio).where(Portfolio.user_id == test_user.id)
|
|
)
|
|
user_portfolios = result.scalars().all()
|
|
|
|
assert len(user_portfolios) == 3
|
|
for p in user_portfolios:
|
|
assert p.user_id == test_user.id
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_query_portfolios_by_type(self, db_session, test_user):
|
|
"""Should retrieve portfolios filtered by type."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
# Create portfolios of different types
|
|
live_portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Live Portfolio",
|
|
portfolio_type=PortfolioType.LIVE,
|
|
initial_capital=Decimal("50000.0000"),
|
|
)
|
|
|
|
paper_portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Paper Portfolio",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
)
|
|
|
|
backtest_portfolio = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Backtest Portfolio",
|
|
portfolio_type=PortfolioType.BACKTEST,
|
|
initial_capital=Decimal("100000.0000"),
|
|
)
|
|
|
|
db_session.add_all([live_portfolio, paper_portfolio, backtest_portfolio])
|
|
await db_session.commit()
|
|
|
|
# Query PAPER portfolios
|
|
result = await db_session.execute(
|
|
select(Portfolio).where(Portfolio.portfolio_type == PortfolioType.PAPER)
|
|
)
|
|
paper_portfolios = result.scalars().all()
|
|
|
|
assert len(paper_portfolios) >= 1
|
|
for p in paper_portfolios:
|
|
assert p.portfolio_type == PortfolioType.PAPER
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_query_active_portfolios(self, db_session, test_user):
|
|
"""Should retrieve only active portfolios."""
|
|
try:
|
|
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
|
|
|
|
# Create active and inactive portfolios
|
|
active = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Active Portfolio",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
is_active=True,
|
|
)
|
|
|
|
inactive = Portfolio(
|
|
user_id=test_user.id,
|
|
name="Inactive Portfolio",
|
|
portfolio_type=PortfolioType.PAPER,
|
|
initial_capital=Decimal("10000.0000"),
|
|
is_active=False,
|
|
)
|
|
|
|
db_session.add_all([active, inactive])
|
|
await db_session.commit()
|
|
|
|
# Query active portfolios
|
|
result = await db_session.execute(
|
|
select(Portfolio).where(
|
|
Portfolio.user_id == test_user.id,
|
|
Portfolio.is_active == True
|
|
)
|
|
)
|
|
active_portfolios = result.scalars().all()
|
|
|
|
assert len(active_portfolios) >= 1
|
|
for p in active_portfolios:
|
|
assert p.is_active is True
|
|
|
|
except ImportError:
|
|
pytest.skip("Portfolio model not yet implemented (TDD RED phase)")
|