Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions quantara/web_app/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,27 @@ async def connect_wallet(
@limiter.limit(USER_DATA_LIMIT)
async def get_session(request: Request, wallet_id: str | None = None):
"""
Endpoint for frontend initialization to verify if a valid httpOnly
Endpoint for frontend initialization to verify if a valid httpOnly
cookie session exists without exposing the raw cookie to client JS.
"""
# Note: Your REPO-002 auth middleware will automatically populate wallet_id from the cookie
if not wallet_id:
if wallet_id:
return {"authenticated": True, "walletId": wallet_id}

session_token = request.cookies.get("wallet_id")
if not session_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
status_code=status.HTTP_401_UNAUTHORIZED,
detail="No active wallet session"
)
return {"authenticated": True, "walletId": wallet_id}

resolved_wallet_id = await session_store.get_wallet_id(session_token)
if not resolved_wallet_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="No active wallet session"
)

return {"authenticated": True, "walletId": resolved_wallet_id}

@router.post("/logout")
@limiter.limit(USER_DATA_LIMIT)
Expand Down
71 changes: 71 additions & 0 deletions quantara/web_app/tests/test_auth_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from unittest.mock import patch, AsyncMock

import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

from web_app.api.auth import router

app = FastAPI()
app.include_router(router)


@pytest.fixture
def client():
return TestClient(app)


def _patch_store(wallet_id):
"""Return a patch context that replaces session_store.get_wallet_id."""
mock = AsyncMock(return_value=wallet_id)
return patch("web_app.api.auth.session_store.get_wallet_id", mock)


class TestGetSessionValidCookie:
def test_valid_cookie_returns_wallet_id(self, client):
with _patch_store("STELLAR-123"):
resp = client.get(
"/api/auth/session",
cookies={"wallet_id": "valid-token"},
)
assert resp.status_code == 200
body = resp.json()
assert body["authenticated"] is True
assert body["walletId"] == "STELLAR-123"


class TestGetSessionMissingCookie:
def test_no_cookie_returns_401(self, client):
resp = client.get("/api/auth/session")
assert resp.status_code == 401
assert resp.json()["detail"] == "No active wallet session"


class TestGetSessionExpiredCookie:
def test_expired_cookie_returns_401(self, client):
with _patch_store(None):
resp = client.get(
"/api/auth/session",
cookies={"wallet_id": "expired-token"},
)
assert resp.status_code == 401
assert resp.json()["detail"] == "No active wallet session"


class TestGetSessionBackwardCompat:
def test_query_param_still_works(self, client):
resp = client.get("/api/auth/session?wallet_id=QRPARAM-456")
assert resp.status_code == 200
body = resp.json()
assert body["authenticated"] is True
assert body["walletId"] == "QRPARAM-456"

def test_query_param_takes_precedence_over_cookie(self, client):
with _patch_store("COOKIE-WALLET") as mock_get:
resp = client.get(
"/api/auth/session?wallet_id=QUERY-WALLET",
cookies={"wallet_id": "cookie-token"},
)
assert resp.status_code == 200
assert resp.json()["walletId"] == "QUERY-WALLET"
mock_get.assert_not_called()
26 changes: 7 additions & 19 deletions quantara/web_app/tests/test_vault.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,9 @@

import pytest
from fastapi.testclient import TestClient
from httpx import ASGITransport, AsyncClient

from web_app.api.main import app
from web_app.db.crud import UserDBConnector

client = TestClient(app)


@pytest.fixture
async def async_client():
"""Fixture that provides an async client for testing."""
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac


@pytest.mark.anyio
@pytest.mark.parametrize(
Expand Down Expand Up @@ -55,7 +43,7 @@ async def test_deposit_to_vault(
expected_status,
expected_response,
mock_user_db_connector,
async_client,
client: TestClient,
):
"""Test vault deposit with different scenarios."""
mock_user = MagicMock()
Expand All @@ -73,9 +61,9 @@ async def test_deposit_to_vault(
"web_app.db.crud.DepositDBConnector.create_vault",
return_value=mock_vault,
):
response = await async_client.post("/api/vault/deposit", json=test_data)
response = client.post("/api/vault/deposit", json=test_data)
else:
response = await async_client.post("/api/vault/deposit", json=test_data)
response = client.post("/api/vault/deposit", json=test_data)

assert response.status_code == expected_status
expected = (
Expand Down Expand Up @@ -112,15 +100,15 @@ async def test_get_vault_balance(
balance,
expected_status,
expected_response,
async_client,
client: TestClient,
):
"""Test vault balance retrieval with different scenarios."""
with patch(
"web_app.db.crud.DepositDBConnector.get_vault_balance",
return_value=balance,
):
url = f"/api/vault/balance?wallet_id={wallet_id}&symbol={symbol}"
response = await async_client.get(url)
response = client.get(url)

assert response.status_code == expected_status
expected = (
Expand Down Expand Up @@ -156,7 +144,7 @@ async def test_get_vault_balance(
],
)
async def test_add_vault_balance(
test_data, expected_status, expected_response, async_client
test_data, expected_status, expected_response, client: TestClient
):
"""Test adding to vault balance with different scenarios."""
mock_vault = MagicMock()
Expand All @@ -171,7 +159,7 @@ async def test_add_vault_balance(
"web_app.db.crud.DepositDBConnector.add_vault_balance",
**patch_kwargs,
):
response = await async_client.post("/api/vault/add_balance", json=test_data)
response = client.post("/api/vault/add_balance", json=test_data)

assert response.status_code == expected_status
expected = (
Expand Down
Loading