From f5a1882de073ea6859c226395daefeb566e9e802 Mon Sep 17 00:00:00 2001 From: stuppie Date: Tue, 1 Sep 2026 12:17:32 -0600 Subject: add a test_magic_link with a super cool fake_gr_api_manager using fastapi dependency_overrides --- jb-ui/src/pages/MagicLink.tsx | 2 +- jb/api/auth.py | 7 +++-- jb/dependencies.py | 8 ++++++ jb/views/auth.py | 16 +++++++---- tests/http/test_auth.py | 66 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 jb/dependencies.py create mode 100644 tests/http/test_auth.py diff --git a/jb-ui/src/pages/MagicLink.tsx b/jb-ui/src/pages/MagicLink.tsx index cf7f87b..4042624 100644 --- a/jb-ui/src/pages/MagicLink.tsx +++ b/jb-ui/src/pages/MagicLink.tsx @@ -25,7 +25,7 @@ const MagicLink = function () { method: "POST", credentials: "include", headers: {"Content-Type": "application/json"}, - body: JSON.stringify({token}), + body: JSON.stringify({"token": token}), }).then((response) => { if (!response.ok) throw new Error("Magic-link exchange failed"); window.location.replace("/"); diff --git a/jb/api/auth.py b/jb/api/auth.py index 3481b30..a92515d 100644 --- a/jb/api/auth.py +++ b/jb/api/auth.py @@ -8,7 +8,8 @@ from fastapi import Depends, HTTPException, Request, Response, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from jb.config import settings -from jb.decorators import gr_api_manager +from jb.dependencies import get_gr_api_manager +from jb.managers.gr_api import GRApiManager from jb.models.auth import User bearer = HTTPBearer(auto_error=False) @@ -22,6 +23,7 @@ JWT_AUDIENCE = "jamesbillings67" def get_authenticated_user( request: Request, credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(bearer)], + gr_api: Annotated[GRApiManager, Depends(get_gr_api_manager)], ) -> User: """FastAPI dependency for endpoints requiring a valid session.""" if settings.session_jwt_secret is None: @@ -65,7 +67,7 @@ def get_authenticated_user( ) product_user_id = claims.get("sub") - user = gr_api_manager.get_user(product_user_id=product_user_id) + user = gr_api.get_user(product_user_id=product_user_id) return user @@ -86,4 +88,3 @@ def create_session(product_user_id: str) -> str: algorithm="HS256", ) - diff --git a/jb/dependencies.py b/jb/dependencies.py new file mode 100644 index 0000000..b9577bc --- /dev/null +++ b/jb/dependencies.py @@ -0,0 +1,8 @@ +"""FastAPI dependency providers shared by application routes.""" + +from jb.decorators import gr_api_manager +from jb.managers.gr_api import GRApiManager + + +def get_gr_api_manager() -> GRApiManager: + return gr_api_manager diff --git a/jb/views/auth.py b/jb/views/auth.py index 06dbdde..33c9452 100644 --- a/jb/views/auth.py +++ b/jb/views/auth.py @@ -18,12 +18,12 @@ from jb.api.auth import ( ) from jb.api.magic_token import consume_magic_token, create_magic_token from jb.config import settings -from jb.decorators import gr_api_manager +from jb.dependencies import get_gr_api_manager +from jb.managers.gr_api import GRApiManager from jb.models.auth import ( - MagicLinkExchangeRequest, AccountLogin, + MagicLinkExchangeRequest, User, - email_to_product_user_id, ) from jb.settings import BASE_HTML @@ -58,13 +58,17 @@ def magic_link_landing_page() -> HTMLResponse: @auth_router.post("/magic-link/exchange", status_code=status.HTTP_204_NO_CONTENT) -def exchange_magic_link(body: MagicLinkExchangeRequest, response: Response) -> None: +def exchange_magic_link( + body: MagicLinkExchangeRequest, + response: Response, + gr_api: Annotated[GRApiManager, Depends(get_gr_api_manager)], +) -> None: """Exchange a magic link only after its landing page makes an explicit POST.""" user_email = consume_magic_token(body.token) - user = User.model_validate({'email': user_email}) + user = User.model_validate({"email": user_email}) # hit thl to make sure this user exists - user = gr_api_manager.ensure_user_exists(user) + user = gr_api.ensure_user_exists(user) session_token = create_session(user.product_user_id) response.set_cookie( diff --git a/tests/http/test_auth.py b/tests/http/test_auth.py new file mode 100644 index 0000000..8a625bf --- /dev/null +++ b/tests/http/test_auth.py @@ -0,0 +1,66 @@ +import secrets +from urllib.parse import parse_qs, urlparse + +import pytest +from httpx import AsyncClient + +from jb.api.auth import SESSION_COOKIE_NAME +from jb.dependencies import get_gr_api_manager +from jb.main import app +from jb.models.auth import User + + +class FakeGRApiManager: + def __init__(self): + self.users: dict[str, User] = {} + + def ensure_user_exists(self, user: User) -> User: + self.users.setdefault(user.product_user_id, user) + return self.users[user.product_user_id] + + def get_user(self, product_user_id: str) -> User: + return self.users[product_user_id] + + +@pytest.fixture +def email() -> str: + email = secrets.token_urlsafe(16) + "@gmail.com" + return email.lower() + + +@pytest.fixture +def fake_gr_api_manager(): + manager = FakeGRApiManager() + app.dependency_overrides[get_gr_api_manager] = lambda: manager + yield manager + app.dependency_overrides.pop(get_gr_api_manager, None) + + +class TestAuth: + @pytest.mark.anyio + async def test_magic_link( + self, + httpxclient: AsyncClient, + fake_gr_api_manager: FakeGRApiManager, + email: str, + ): + client = httpxclient + + res = await client.post( + "/auth/magic-link/request", json={"email": email} + ) + d = res.json() + assert res.status_code == 200 + assert d["magic_link"] + + token = parse_qs(urlparse(d["magic_link"]).query)["token"][0] + + url = "/auth/magic-link/exchange" + body = {"token": token} + res = await client.post(url, json=body) + assert res.status_code == 204 + assert client.cookies.get(SESSION_COOKIE_NAME) + + res = await client.get("/auth/session") + assert res.status_code == 200 + assert res.json()["email"] == email -- cgit v1.2.3