"""Integration tests for account tools."""
import pytest
import respx
from httpx import Response
from schwab_mcp.tools import account
# Sample API responses
ACCOUNT_NUMBERS_RESPONSE = [
{"accountNumber": "12345678", "hashValue": "ABC123HASH"},
{"accountNumber": "87654321", "hashValue": "XYZ789HASH"},
]
ACCOUNT_RESPONSE = {
"securitiesAccount": {
"type": "INDIVIDUAL",
"accountNumber": "12345678",
"currentBalances": {
"availableFunds": 10000.00,
"cashBalance": 5000.00,
"longMarketValue": 50000.00,
"liquidationValue": 55000.00,
"buyingPower": 20000.00,
},
}
}
POSITIONS_RESPONSE = {
"securitiesAccount": {
"type": "INDIVIDUAL",
"accountNumber": "12345678",
"positions": [
{
"instrument": {
"symbol": "AAPL",
"description": "Apple Inc",
"assetType": "EQUITY",
},
"longQuantity": 100,
"shortQuantity": 0,
"marketValue": 18500.00,
"averagePrice": 175.00,
"averageCostBasis": 175.00,
"currentDayProfitLoss": 150.00,
"currentDayProfitLossPercentage": 0.82,
},
{
"instrument": {
"symbol": "MSFT",
"description": "Microsoft Corp",
"assetType": "EQUITY",
},
"longQuantity": 50,
"shortQuantity": 0,
"marketValue": 21000.00,
"averagePrice": 400.00,
"averageCostBasis": 400.00,
"currentDayProfitLoss": -200.00,
"currentDayProfitLossPercentage": -0.94,
},
],
}
}
class TestGetPositions:
"""Tests for get_positions tool."""
@respx.mock
@pytest.mark.asyncio
async def test_get_positions_default_account(self, mock_client):
"""Test getting positions for default (first) account."""
# Mock account numbers endpoint
respx.get(
"https://api.schwabapi.com/trader/v1/accounts/accountNumbers"
).mock(return_value=Response(200, json=ACCOUNT_NUMBERS_RESPONSE))
# Mock account with positions endpoint
respx.get(
"https://api.schwabapi.com/trader/v1/accounts/ABC123HASH"
).mock(return_value=Response(200, json=POSITIONS_RESPONSE))
result = await account.get_positions(mock_client, {})
assert result["account_id"] == "ABC123HASH"
assert len(result["positions"]) == 2
# Check first position
aapl = result["positions"][0]
assert aapl["symbol"] == "AAPL"
assert aapl["quantity"] == 100
assert aapl["market_value"] == 18500.00
assert aapl["cost_per_share"] == 175.00
assert aapl["cost_basis"] == 17500.00 # 175 * 100
assert aapl["gain_loss"] == 1000.00 # 18500 - 17500
assert aapl["day_change"] == 150.00
@respx.mock
@pytest.mark.asyncio
async def test_get_positions_specific_account(self, mock_client):
"""Test getting positions for a specific account."""
# Mock account with positions endpoint
respx.get(
"https://api.schwabapi.com/trader/v1/accounts/SPECIFIC_HASH"
).mock(return_value=Response(200, json=POSITIONS_RESPONSE))
result = await account.get_positions(
mock_client, {"account_id": "SPECIFIC_HASH"}
)
assert result["account_id"] == "SPECIFIC_HASH"
assert len(result["positions"]) == 2
@respx.mock
@pytest.mark.asyncio
async def test_get_positions_empty(self, mock_client):
"""Test getting positions when account has no positions."""
empty_response = {
"securitiesAccount": {
"type": "INDIVIDUAL",
"positions": [],
}
}
respx.get(
"https://api.schwabapi.com/trader/v1/accounts/accountNumbers"
).mock(return_value=Response(200, json=ACCOUNT_NUMBERS_RESPONSE))
respx.get(
"https://api.schwabapi.com/trader/v1/accounts/ABC123HASH"
).mock(return_value=Response(200, json=empty_response))
result = await account.get_positions(mock_client, {})
assert result["positions"] == []
class TestGetAccount:
"""Tests for get_account tool."""
@respx.mock
@pytest.mark.asyncio
async def test_get_account_default(self, mock_client):
"""Test getting account info for default account."""
respx.get(
"https://api.schwabapi.com/trader/v1/accounts/accountNumbers"
).mock(return_value=Response(200, json=ACCOUNT_NUMBERS_RESPONSE))
respx.get(
"https://api.schwabapi.com/trader/v1/accounts/ABC123HASH"
).mock(return_value=Response(200, json=ACCOUNT_RESPONSE))
result = await account.get_account(mock_client, {})
assert result["account_id"] == "ABC123HASH"
assert result["account_type"] == "INDIVIDUAL"
assert result["is_taxable"] is True
assert result["balances"]["cash_available"] == 10000.00
assert result["balances"]["cash_balance"] == 5000.00
assert result["balances"]["market_value"] == 50000.00
assert result["balances"]["total_value"] == 55000.00
assert result["balances"]["buying_power"] == 20000.00
@respx.mock
@pytest.mark.asyncio
async def test_get_account_ira_not_taxable(self, mock_client):
"""Test that IRA accounts are marked as non-taxable."""
ira_response = {
"securitiesAccount": {
"type": "IRA",
"currentBalances": {
"availableFunds": 5000.00,
},
}
}
respx.get(
"https://api.schwabapi.com/trader/v1/accounts/accountNumbers"
).mock(return_value=Response(200, json=ACCOUNT_NUMBERS_RESPONSE))
respx.get(
"https://api.schwabapi.com/trader/v1/accounts/ABC123HASH"
).mock(return_value=Response(200, json=ira_response))
result = await account.get_account(mock_client, {})
assert result["account_type"] == "IRA"
assert result["is_taxable"] is False
@respx.mock
@pytest.mark.asyncio
async def test_get_account_no_accounts_error(self, mock_client):
"""Test error when no accounts are found."""
respx.get(
"https://api.schwabapi.com/trader/v1/accounts/accountNumbers"
).mock(return_value=Response(200, json=[]))
with pytest.raises(ValueError, match="No accounts found"):
await account.get_account(mock_client, {})