From 0c7558393048568e4abba6a139488bf79c41d0c7 Mon Sep 17 00:00:00 2001 From: counterweight Date: Tue, 23 Dec 2025 15:52:02 +0100 Subject: [PATCH] Add endpoint to get a single trade by ID --- backend/routes/exchange.py | 23 +++++++++++ backend/tests/test_exchange.py | 73 ++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/backend/routes/exchange.py b/backend/routes/exchange.py index 086128c..c7fe4f5 100644 --- a/backend/routes/exchange.py +++ b/backend/routes/exchange.py @@ -565,6 +565,29 @@ async def get_my_trades( return [_to_exchange_response(ex, current_user.email) for ex in exchanges] +@trades_router.get("/{exchange_id}", response_model=ExchangeResponse) +async def get_my_trade( + exchange_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(require_permission(Permission.VIEW_OWN_EXCHANGES)), +) -> ExchangeResponse: + """Get a specific trade by ID. User can only access their own trades.""" + result = await db.execute( + select(Exchange).where( + and_(Exchange.id == exchange_id, Exchange.user_id == current_user.id) + ) + ) + exchange = result.scalar_one_or_none() + + if not exchange: + raise HTTPException( + status_code=404, + detail="Trade not found or you don't have permission to view it.", + ) + + return _to_exchange_response(exchange, current_user.email) + + @trades_router.post("/{exchange_id}/cancel", response_model=ExchangeResponse) async def cancel_my_trade( exchange_id: int, diff --git a/backend/tests/test_exchange.py b/backend/tests/test_exchange.py index d023fa5..1071abe 100644 --- a/backend/tests/test_exchange.py +++ b/backend/tests/test_exchange.py @@ -646,6 +646,79 @@ class TestUserTrades: assert len(data) == 2 +class TestGetMyTrade: + """Test getting a single trade by ID.""" + + @pytest.mark.asyncio + async def test_get_my_trade_success(self, client_factory, regular_user, admin_user): + """Can get own trade by ID.""" + target_date = await setup_availability_and_price(client_factory, admin_user) + + with mock_price_fetcher(20000.0): + async with client_factory.create(cookies=regular_user["cookies"]) as client: + # Create a trade + create_response = await client.post( + "/api/exchange", + json={ + "slot_start": f"{target_date}T09:00:00Z", + "direction": "buy", + "bitcoin_transfer_method": "onchain", + "eur_amount": 10000, + }, + ) + assert create_response.status_code == 200 + trade_id = create_response.json()["id"] + + # Get the trade + get_response = await client.get(f"/api/trades/{trade_id}") + + assert get_response.status_code == 200 + data = get_response.json() + assert data["id"] == trade_id + assert data["direction"] == "buy" + assert data["bitcoin_transfer_method"] == "onchain" + + @pytest.mark.asyncio + async def test_cannot_get_other_user_trade( + self, client_factory, regular_user, alt_regular_user, admin_user + ): + """Cannot get another user's trade.""" + target_date = await setup_availability_and_price(client_factory, admin_user) + + with mock_price_fetcher(20000.0): + # First user creates a trade + async with client_factory.create(cookies=regular_user["cookies"]) as client: + create_response = await client.post( + "/api/exchange", + json={ + "slot_start": f"{target_date}T09:00:00Z", + "direction": "buy", + "bitcoin_transfer_method": "onchain", + "eur_amount": 10000, + }, + ) + assert create_response.status_code == 200 + trade_id = create_response.json()["id"] + + # Second user tries to get it + async with client_factory.create( + cookies=alt_regular_user["cookies"] + ) as client: + get_response = await client.get(f"/api/trades/{trade_id}") + + assert get_response.status_code == 404 + + @pytest.mark.asyncio + async def test_get_nonexistent_trade_returns_404( + self, client_factory, regular_user + ): + """Getting a nonexistent trade returns 404.""" + async with client_factory.create(cookies=regular_user["cookies"]) as client: + response = await client.get("/api/trades/99999") + + assert response.status_code == 404 + + class TestCancelTrade: """Test cancelling trades."""