From fafe36165657c06c5b67576c201d89ae5053fbbc Mon Sep 17 00:00:00 2001 From: portable Date: Thu, 20 Aug 2026 17:35:18 +0100 Subject: [PATCH 1/2] fix(auth): read wallet_id cookie in session restore endpoint - GET /api/auth/session now reads the httpOnly wallet_id cookie - Falls back to session_store.get_wallet_id(token) for Redis lookup - Backward-compatible: query param still works if provided - Removed stale REPO-002 comment - Added 5 tests covering valid/missing/expired cookie and query param Closes #419 --- quantara/web_app/api/auth.py | 21 ++++-- quantara/web_app/tests/test_auth_session.py | 71 +++++++++++++++++++++ 2 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 quantara/web_app/tests/test_auth_session.py diff --git a/quantara/web_app/api/auth.py b/quantara/web_app/api/auth.py index bad4afd8..d91051ae 100644 --- a/quantara/web_app/api/auth.py +++ b/quantara/web_app/api/auth.py @@ -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) diff --git a/quantara/web_app/tests/test_auth_session.py b/quantara/web_app/tests/test_auth_session.py new file mode 100644 index 00000000..7dd2c0e1 --- /dev/null +++ b/quantara/web_app/tests/test_auth_session.py @@ -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() From 59831a7585e3b97260452cf946ac4aaab81f72b7 Mon Sep 17 00:00:00 2001 From: amberly-d Date: Fri, 21 Aug 2026 14:38:57 +0100 Subject: [PATCH 2/2] fix(tests): restore test_vault.py to use sync TestClient The inherited async_client fixture from the outdated base causes RuntimeError: must be called from async context when running under trio. Restore the original sync TestClient approach that works correctly. --- quantara/web_app/tests/test_vault.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/quantara/web_app/tests/test_vault.py b/quantara/web_app/tests/test_vault.py index ad989e5d..c03cb338 100644 --- a/quantara/web_app/tests/test_vault.py +++ b/quantara/web_app/tests/test_vault.py @@ -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( @@ -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() @@ -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 = ( @@ -112,7 +100,7 @@ async def test_get_vault_balance( balance, expected_status, expected_response, - async_client, + client: TestClient, ): """Test vault balance retrieval with different scenarios.""" with patch( @@ -120,7 +108,7 @@ async def test_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 = ( @@ -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() @@ -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 = (