aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorstuppie2026-09-01 12:17:32 -0600
committerstuppie2026-09-01 12:17:32 -0600
commitf5a1882de073ea6859c226395daefeb566e9e802 (patch)
tree039c0534a4d051eb3c0efc25603bd635457c103e
parentc991d4f51254b1c81fa6e0e19b0ba94b8c5f61c4 (diff)
downloadamt-jb-f5a1882de073ea6859c226395daefeb566e9e802.tar.gz
amt-jb-f5a1882de073ea6859c226395daefeb566e9e802.zip
add a test_magic_link with a super cool fake_gr_api_manager using fastapi dependency_overrides
-rw-r--r--jb-ui/src/pages/MagicLink.tsx2
-rw-r--r--jb/api/auth.py7
-rw-r--r--jb/dependencies.py8
-rw-r--r--jb/views/auth.py16
-rw-r--r--tests/http/test_auth.py66
5 files changed, 89 insertions, 10 deletions
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