Add paginated responses (items/total/skip/limit) to:
- GET /api/data/stocks/{ticker}/prices (default limit=365)
- GET /api/data/etfs/{ticker}/prices (default limit=365)
- GET /api/portfolios/{id}/snapshots (default limit=100)
- GET /api/portfolios/{id}/transactions (default limit=50)
Frontend: update snapshot/transaction consumers to handle new response
shape, add "Load more" button to transaction table.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
218 lines
6.4 KiB
Python
218 lines
6.4 KiB
Python
"""
|
|
E2E tests for portfolio management flow.
|
|
"""
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
def test_portfolio_crud_flow(client: TestClient, auth_headers):
|
|
"""Test complete portfolio CRUD flow."""
|
|
# Create portfolio
|
|
response = client.post(
|
|
"/api/portfolios",
|
|
json={
|
|
"name": "Test Portfolio",
|
|
"portfolio_type": "pension",
|
|
},
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 201
|
|
portfolio = response.json()
|
|
portfolio_id = portfolio["id"]
|
|
assert portfolio["name"] == "Test Portfolio"
|
|
assert portfolio["portfolio_type"] == "pension"
|
|
|
|
# Get portfolio list
|
|
response = client.get("/api/portfolios", headers=auth_headers)
|
|
assert response.status_code == 200
|
|
portfolios = response.json()
|
|
assert len(portfolios) == 1
|
|
assert portfolios[0]["id"] == portfolio_id
|
|
|
|
# Get single portfolio
|
|
response = client.get(f"/api/portfolios/{portfolio_id}", headers=auth_headers)
|
|
assert response.status_code == 200
|
|
assert response.json()["id"] == portfolio_id
|
|
|
|
# Update portfolio
|
|
response = client.put(
|
|
f"/api/portfolios/{portfolio_id}",
|
|
json={"name": "Updated Portfolio"},
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "Updated Portfolio"
|
|
|
|
# Delete portfolio
|
|
response = client.delete(f"/api/portfolios/{portfolio_id}", headers=auth_headers)
|
|
assert response.status_code == 204
|
|
|
|
# Verify deleted
|
|
response = client.get(f"/api/portfolios/{portfolio_id}", headers=auth_headers)
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_targets_flow(client: TestClient, auth_headers):
|
|
"""Test portfolio target allocation flow."""
|
|
# Create portfolio
|
|
response = client.post(
|
|
"/api/portfolios",
|
|
json={"name": "Target Test Portfolio", "portfolio_type": "general"},
|
|
headers=auth_headers,
|
|
)
|
|
portfolio_id = response.json()["id"]
|
|
|
|
# Set targets
|
|
targets = [
|
|
{"ticker": "005930", "target_ratio": 40},
|
|
{"ticker": "000660", "target_ratio": 30},
|
|
{"ticker": "035420", "target_ratio": 30},
|
|
]
|
|
response = client.put(
|
|
f"/api/portfolios/{portfolio_id}/targets",
|
|
json=targets,
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
result = response.json()
|
|
assert len(result) == 3
|
|
assert sum(float(t["target_ratio"]) for t in result) == 100
|
|
|
|
# Get targets
|
|
response = client.get(
|
|
f"/api/portfolios/{portfolio_id}/targets",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
assert len(response.json()) == 3
|
|
|
|
# Test invalid targets (not 100%)
|
|
invalid_targets = [
|
|
{"ticker": "005930", "target_ratio": 50},
|
|
{"ticker": "000660", "target_ratio": 30},
|
|
]
|
|
response = client.put(
|
|
f"/api/portfolios/{portfolio_id}/targets",
|
|
json=invalid_targets,
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
|
|
def test_holdings_flow(client: TestClient, auth_headers):
|
|
"""Test portfolio holdings flow."""
|
|
# Create portfolio
|
|
response = client.post(
|
|
"/api/portfolios",
|
|
json={"name": "Holdings Test Portfolio", "portfolio_type": "general"},
|
|
headers=auth_headers,
|
|
)
|
|
portfolio_id = response.json()["id"]
|
|
|
|
# Set holdings
|
|
holdings = [
|
|
{"ticker": "005930", "quantity": 10, "avg_price": 70000},
|
|
{"ticker": "000660", "quantity": 5, "avg_price": 120000},
|
|
]
|
|
response = client.put(
|
|
f"/api/portfolios/{portfolio_id}/holdings",
|
|
json=holdings,
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
result = response.json()
|
|
assert len(result) == 2
|
|
|
|
# Get holdings
|
|
response = client.get(
|
|
f"/api/portfolios/{portfolio_id}/holdings",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
assert len(response.json()) == 2
|
|
|
|
|
|
def test_transaction_flow(client: TestClient, auth_headers):
|
|
"""Test transaction recording flow."""
|
|
# Create portfolio with initial holdings
|
|
response = client.post(
|
|
"/api/portfolios",
|
|
json={"name": "Transaction Test Portfolio", "portfolio_type": "general"},
|
|
headers=auth_headers,
|
|
)
|
|
portfolio_id = response.json()["id"]
|
|
|
|
# Buy transaction
|
|
response = client.post(
|
|
f"/api/portfolios/{portfolio_id}/transactions",
|
|
json={
|
|
"ticker": "005930",
|
|
"tx_type": "buy",
|
|
"quantity": 10,
|
|
"price": 70000,
|
|
"executed_at": "2024-01-15T10:00:00",
|
|
"memo": "Initial purchase",
|
|
},
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 201
|
|
tx = response.json()
|
|
assert tx["ticker"] == "005930"
|
|
assert tx["tx_type"] == "buy"
|
|
|
|
# Verify holdings updated
|
|
response = client.get(
|
|
f"/api/portfolios/{portfolio_id}/holdings",
|
|
headers=auth_headers,
|
|
)
|
|
holdings = response.json()
|
|
assert len(holdings) == 1
|
|
assert holdings[0]["ticker"] == "005930"
|
|
assert holdings[0]["quantity"] == 10
|
|
|
|
# Sell transaction
|
|
response = client.post(
|
|
f"/api/portfolios/{portfolio_id}/transactions",
|
|
json={
|
|
"ticker": "005930",
|
|
"tx_type": "sell",
|
|
"quantity": 5,
|
|
"price": 75000,
|
|
"executed_at": "2024-01-16T10:00:00",
|
|
},
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
# Verify holdings updated
|
|
response = client.get(
|
|
f"/api/portfolios/{portfolio_id}/holdings",
|
|
headers=auth_headers,
|
|
)
|
|
holdings = response.json()
|
|
assert holdings[0]["quantity"] == 5
|
|
|
|
# Get transactions
|
|
response = client.get(
|
|
f"/api/portfolios/{portfolio_id}/transactions",
|
|
headers=auth_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
txs = response.json()["items"]
|
|
assert len(txs) == 2
|
|
|
|
|
|
def test_unauthorized_access(client: TestClient, auth_headers):
|
|
"""Test that users can't access other users' portfolios."""
|
|
# Create portfolio with test user
|
|
response = client.post(
|
|
"/api/portfolios",
|
|
json={"name": "Private Portfolio", "portfolio_type": "general"},
|
|
headers=auth_headers,
|
|
)
|
|
portfolio_id = response.json()["id"]
|
|
|
|
# Try to access without auth
|
|
response = client.get(f"/api/portfolios/{portfolio_id}")
|
|
assert response.status_code == 401
|