NoteStack-Test / backend /tests /test_auth.py
Arslan1997's picture
fixed auth
013ff92
Raw History Blame
6.8 kB
from app.services.email import ConsoleEmailProvider
from tests.conftest import last_code
def _signup(client, email="writer@example.com", password="stardust-42"):
r = client.post("/api/auth/email/register/start", json={"email": email, "password": password, "name": "Ada L"})
assert r.status_code == 200, r.text
r = client.post("/api/auth/email/register/verify", json={"email": email, "code": last_code()})
assert r.status_code == 200, r.text
return r.json()
def test_signup_login_me_logout(client):
data = _signup(client)
assert data["created"] is True
assert any(e.subject == "Welcome to Notestack" for e in ConsoleEmailProvider.sent)
headers = {"Authorization": f"Bearer {data['access_token']}"}
assert client.get("/api/auth/me", headers=headers).json()["email"] == "writer@example.com"
r = client.post("/api/auth/email/login", json={"email": "WRITER@example.com", "password": "stardust-42"})
assert r.status_code == 200
assert client.post("/api/auth/logout", headers=headers).status_code == 200
assert client.get("/api/auth/me", headers=headers).status_code == 401 # token_version bumped
def test_wrong_code_and_attempt_limit(client):
client.post("/api/auth/email/register/start", json={"email": "a@b.co", "password": "longenough"})
for _ in range(5):
r = client.post("/api/auth/email/register/verify", json={"email": "a@b.co", "code": "000000"})
if last_code() == "000000":
return # astronomically unlikely collision
assert r.status_code == 400
r = client.post("/api/auth/email/register/verify", json={"email": "a@b.co", "code": last_code()})
assert r.json()["detail"]["code"] == "too_many_attempts"
def test_resend_cooldown(client):
client.post("/api/auth/email/register/start", json={"email": "c@d.co", "password": "longenough"})
r = client.post("/api/auth/email/register/resend", json={"email": "c@d.co"})
assert r.status_code == 429
def test_bad_password_and_weak_password(client):
_signup(client)
r = client.post("/api/auth/email/login", json={"email": "writer@example.com", "password": "nope-nope"})
assert r.status_code == 401
r = client.post("/api/auth/email/register/start", json={"email": "x@y.co", "password": "short"})
assert r.status_code == 400
def test_password_reset_revokes_old_tokens(client):
data = _signup(client)
old = {"Authorization": f"Bearer {data['access_token']}"}
client.post("/api/auth/password/forgot/start", json={"email": "writer@example.com"})
code = last_code()
r = client.post("/api/auth/password/forgot/check", json={"email": "writer@example.com", "code": code})
assert r.status_code == 200
r = client.post("/api/auth/password/forgot/complete",
json={"email": "writer@example.com", "code": code, "password": "new-orbit-99"})
assert r.status_code == 200
assert client.get("/api/auth/me", headers=old).status_code == 401
def test_google_user_cannot_use_password(client, db_session):
from app.services.auth_identity import resolve_or_create_google_user
resolve_or_create_google_user(db_session, email="g@example.com", google_id="123", name="G", avatar_url=None)
db_session.commit()
r = client.post("/api/auth/email/register/start", json={"email": "g@example.com", "password": "longenough"})
assert r.status_code == 409
assert r.json()["detail"]["code"] == "wrong_provider"
def test_delete_account(client):
data = _signup(client)
headers = {"Authorization": f"Bearer {data['access_token']}"}
assert client.post("/api/auth/delete-account", headers=headers).status_code == 200
r = client.post("/api/auth/email/login", json={"email": "writer@example.com", "password": "stardust-42"})
assert r.status_code == 403
def _google_redirect(client, monkeypatch, claims: dict, next_path="/app/notebooks"):
from urllib.parse import parse_qs, urlparse
from app.config import settings
from app.routers import auth as auth_router
monkeypatch.setattr(settings, "google_client_id", "cid")
monkeypatch.setattr(settings, "google_client_secret", "secret")
assert client.get("/api/auth/providers").json() == {"google": "redirect"}
r = client.get("/api/auth/google/start", params={"next": next_path}, follow_redirects=False)
assert r.status_code == 302
q = parse_qs(urlparse(r.headers["location"]).query)
assert q["redirect_uri"] == ["http://localhost:8000/api/auth/google/callback"]
monkeypatch.setattr(auth_router, "exchange_google_code", lambda code: {"nonce": q["nonce"][0], **claims})
r = client.get("/api/auth/google/callback", params={"code": "abc", "state": q["state"][0]},
follow_redirects=False)
assert r.status_code == 302
location = urlparse(r.headers["location"])
return location, parse_qs(location.fragment)
def test_google_redirect_flow(client, monkeypatch):
claims = {"email": "Star@Example.com", "email_verified": True, "sub": "g-1", "name": "Star"}
location, frag = _google_redirect(client, monkeypatch, claims)
assert location.path == "/auth/callback"
assert frag["next"] == ["/app/notebooks"]
r = client.post("/api/auth/ticket", json={"ticket": frag["ticket"][0]})
assert r.status_code == 200
data = r.json()
assert data["created"] is True and data["user"]["email"] == "star@example.com"
me = client.get("/api/auth/me", headers={"Authorization": f"Bearer {data['access_token']}"})
assert me.json()["auth_provider"] == "google"
# A ticket is not a session token.
assert client.get("/api/auth/me", headers={"Authorization": f"Bearer {frag['ticket'][0]}"}).status_code == 401
def test_google_redirect_rejects_open_redirect_and_email_accounts(client, monkeypatch):
_signup(client, email="taken@example.com")
claims = {"email": "taken@example.com", "email_verified": True, "sub": "g-2"}
location, frag = _google_redirect(client, monkeypatch, claims, next_path="//evil.example")
assert location.path == "/auth"
assert "password" in frag["error"][0]
def test_google_callback_requires_state_cookie(client, monkeypatch):
from datetime import timedelta
from app.auth import encode_signed
state = encode_signed({"typ": "oauth_state", "nonce": "n", "next": "/app"}, timedelta(minutes=5))
r = client.get("/api/auth/google/callback", params={"code": "abc", "state": state}, follow_redirects=False)
assert r.headers["location"].startswith("http://localhost:5173/auth#error=")
def test_safe_next():
from app.routers.auth import _safe_next
assert _safe_next("/app/notebooks/1?x=1") == "/app/notebooks/1?x=1"
for bad in (None, "", "https://evil.example", "//evil.example", r"/\evil.example"):
assert _safe_next(bad) == "/app"