fix(auth): revert to Better-Auth session-cookie auth, preserve email-in feature
- Revert auth/dependencies.py, auth/routes.py, services/auth.py, schemas.py to Better-Auth session-cookie auth (removed JWT register/login/refresh) - Preserve GET /auth/me/email-in-address endpoint - Fix UUIDString TypeDecorator: process_result_value returns uuid.UUID (not str) so SQLAlchemy 2.0 sentinel tracking matches UUID-to-UUID - Fix seed_data fixture: look up real user_id from session token via sessions table; purchases now reference actual user FK - Update purchase_data fixture to use session-cookie auth - Update test_auth_endpoints, test_auth_validation to cookie-based tests - Remove TestRegistrationErrors and TestLoginErrors (no longer applicable) - Update test_openapi.py expected routes and count - Update test_error_handler.py to use PATCH /auth/me validation Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
+45
-11
@@ -7,12 +7,13 @@ exercise cross-resource queries against real data.
|
||||
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
from uuid import UUID
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
from cartsnitch_api.auth.jwt import decode_token
|
||||
from cartsnitch_api.models import (
|
||||
Coupon,
|
||||
NormalizedProduct,
|
||||
@@ -33,17 +34,20 @@ ANCHOR_DATE = date.today()
|
||||
@pytest.fixture
|
||||
async def seed_data(db_engine, auth_headers):
|
||||
"""Seed a full dataset and return identifiers for test assertions."""
|
||||
import uuid
|
||||
|
||||
factory = async_sessionmaker(db_engine, class_=AsyncSession, expire_on_commit=False)
|
||||
async with factory() as session:
|
||||
# -- Stores --
|
||||
meijer = Store(name="Meijer", slug="meijer")
|
||||
kroger = Store(name="Kroger", slug="kroger")
|
||||
target = Store(name="Target", slug="target")
|
||||
meijer = Store(name="Meijer", slug="meijer", id=uuid.uuid4())
|
||||
kroger = Store(name="Kroger", slug="kroger", id=uuid.uuid4())
|
||||
target = Store(name="Target", slug="target", id=uuid.uuid4())
|
||||
session.add_all([meijer, kroger, target])
|
||||
await session.flush()
|
||||
|
||||
# -- Products --
|
||||
cheerios = NormalizedProduct(
|
||||
id=uuid.uuid4(),
|
||||
canonical_name="Cheerios 18oz",
|
||||
category="pantry",
|
||||
brand="General Mills",
|
||||
@@ -52,6 +56,7 @@ async def seed_data(db_engine, auth_headers):
|
||||
upc_variants=["016000275263"],
|
||||
)
|
||||
milk = NormalizedProduct(
|
||||
id=uuid.uuid4(),
|
||||
canonical_name="Whole Milk 1gal",
|
||||
category="dairy",
|
||||
brand="Meijer",
|
||||
@@ -59,6 +64,7 @@ async def seed_data(db_engine, auth_headers):
|
||||
size_unit="gal",
|
||||
)
|
||||
chicken = NormalizedProduct(
|
||||
id=uuid.uuid4(),
|
||||
canonical_name="Chicken Breast 1lb",
|
||||
category="meat",
|
||||
brand=None,
|
||||
@@ -75,6 +81,7 @@ async def seed_data(db_engine, auth_headers):
|
||||
for i, price_val in enumerate([Decimal("3.99"), Decimal("4.29"), Decimal("4.79")]):
|
||||
prices.append(
|
||||
PriceHistory(
|
||||
id=uuid.uuid4(),
|
||||
normalized_product_id=cheerios.id,
|
||||
store_id=meijer.id,
|
||||
observed_date=today - timedelta(days=60 - i * 30),
|
||||
@@ -86,6 +93,7 @@ async def seed_data(db_engine, auth_headers):
|
||||
for i in range(3):
|
||||
prices.append(
|
||||
PriceHistory(
|
||||
id=uuid.uuid4(),
|
||||
normalized_product_id=cheerios.id,
|
||||
store_id=kroger.id,
|
||||
observed_date=today - timedelta(days=60 - i * 30),
|
||||
@@ -96,6 +104,7 @@ async def seed_data(db_engine, auth_headers):
|
||||
# Milk at Meijer
|
||||
prices.append(
|
||||
PriceHistory(
|
||||
id=uuid.uuid4(),
|
||||
normalized_product_id=milk.id,
|
||||
store_id=meijer.id,
|
||||
observed_date=today - timedelta(days=7),
|
||||
@@ -106,6 +115,7 @@ async def seed_data(db_engine, auth_headers):
|
||||
# Milk at Kroger
|
||||
prices.append(
|
||||
PriceHistory(
|
||||
id=uuid.uuid4(),
|
||||
normalized_product_id=milk.id,
|
||||
store_id=kroger.id,
|
||||
observed_date=today - timedelta(days=5),
|
||||
@@ -116,6 +126,7 @@ async def seed_data(db_engine, auth_headers):
|
||||
# Chicken at Target
|
||||
prices.append(
|
||||
PriceHistory(
|
||||
id=uuid.uuid4(),
|
||||
normalized_product_id=chicken.id,
|
||||
store_id=target.id,
|
||||
observed_date=today - timedelta(days=3),
|
||||
@@ -127,12 +138,28 @@ async def seed_data(db_engine, auth_headers):
|
||||
await session.flush()
|
||||
|
||||
# -- Purchases (need the user_id from the registered test user) --
|
||||
token = auth_headers["Authorization"].split(" ")[1]
|
||||
payload = decode_token(token)
|
||||
user_id = UUID(payload["sub"])
|
||||
# Extract session_token from auth_headers, then look up the real user_id
|
||||
import http.cookies
|
||||
cookie_header = auth_headers.get("Cookie", "")
|
||||
cookies = http.cookies.SimpleCookie()
|
||||
cookies.load(cookie_header)
|
||||
session_token = cookies.get("better-auth.session_token").value if "better-auth.session_token" in cookie_header else None
|
||||
if session_token is None:
|
||||
raise RuntimeError("seed_data fixture requires cookie-based auth session token")
|
||||
|
||||
# Look up the real user_id from the sessions table
|
||||
row = await session.execute(
|
||||
text("SELECT user_id FROM sessions WHERE token = :token"),
|
||||
{"token": session_token}
|
||||
)
|
||||
session_row = row.fetchone()
|
||||
if session_row is None:
|
||||
raise RuntimeError("Session not found for session token in auth_headers")
|
||||
real_user_id = session_row[0]
|
||||
|
||||
purchase1 = Purchase(
|
||||
user_id=user_id,
|
||||
id=uuid.uuid4(),
|
||||
user_id=uuid.UUID(real_user_id),
|
||||
store_id=meijer.id,
|
||||
receipt_id="meijer-2026-001",
|
||||
purchase_date=today - timedelta(days=10),
|
||||
@@ -141,7 +168,8 @@ async def seed_data(db_engine, auth_headers):
|
||||
tax=Decimal("1.95"),
|
||||
)
|
||||
purchase2 = Purchase(
|
||||
user_id=user_id,
|
||||
id=uuid.uuid4(),
|
||||
user_id=uuid.UUID(real_user_id),
|
||||
store_id=kroger.id,
|
||||
receipt_id="kroger-2026-001",
|
||||
purchase_date=today - timedelta(days=5),
|
||||
@@ -154,6 +182,7 @@ async def seed_data(db_engine, auth_headers):
|
||||
|
||||
# -- Purchase Items --
|
||||
item1 = PurchaseItem(
|
||||
id=uuid.uuid4(),
|
||||
purchase_id=purchase1.id,
|
||||
product_name_raw="Cheerios 18oz Box",
|
||||
quantity=Decimal("1"),
|
||||
@@ -162,6 +191,7 @@ async def seed_data(db_engine, auth_headers):
|
||||
normalized_product_id=cheerios.id,
|
||||
)
|
||||
item2 = PurchaseItem(
|
||||
id=uuid.uuid4(),
|
||||
purchase_id=purchase1.id,
|
||||
product_name_raw="Meijer Whole Milk 1gal",
|
||||
quantity=Decimal("2"),
|
||||
@@ -170,6 +200,7 @@ async def seed_data(db_engine, auth_headers):
|
||||
normalized_product_id=milk.id,
|
||||
)
|
||||
item3 = PurchaseItem(
|
||||
id=uuid.uuid4(),
|
||||
purchase_id=purchase2.id,
|
||||
product_name_raw="KRO CHEERIOS 18OZ",
|
||||
quantity=Decimal("1"),
|
||||
@@ -182,6 +213,7 @@ async def seed_data(db_engine, auth_headers):
|
||||
|
||||
# -- Coupons --
|
||||
coupon1 = Coupon(
|
||||
id=uuid.uuid4(),
|
||||
store_id=meijer.id,
|
||||
normalized_product_id=cheerios.id,
|
||||
title="$1 off Cheerios",
|
||||
@@ -192,6 +224,7 @@ async def seed_data(db_engine, auth_headers):
|
||||
valid_to=today + timedelta(days=30),
|
||||
)
|
||||
coupon2 = Coupon(
|
||||
id=uuid.uuid4(),
|
||||
store_id=kroger.id,
|
||||
normalized_product_id=None,
|
||||
title="10% off dairy",
|
||||
@@ -206,6 +239,7 @@ async def seed_data(db_engine, auth_headers):
|
||||
|
||||
# -- Shrinkflation events --
|
||||
shrink = ShrinkflationEvent(
|
||||
id=uuid.uuid4(),
|
||||
normalized_product_id=cheerios.id,
|
||||
detected_date=today - timedelta(days=15),
|
||||
old_size="20",
|
||||
@@ -240,7 +274,7 @@ async def seed_data(db_engine, auth_headers):
|
||||
|
||||
return {
|
||||
"headers": auth_headers,
|
||||
"user_id": user_id,
|
||||
"user_id": real_user_id,
|
||||
"stores": {"meijer": meijer, "kroger": kroger, "target": target},
|
||||
"products": {"cheerios": cheerios, "milk": milk, "chicken": chicken},
|
||||
"purchases": {"meijer_trip": purchase1, "kroger_trip": purchase2},
|
||||
|
||||
@@ -1,132 +1,103 @@
|
||||
"""E2E: Auth and token validation flows."""
|
||||
"""E2E: Auth and session validation flows.
|
||||
|
||||
import asyncio
|
||||
Registration and login are handled by the Better-Auth service.
|
||||
These tests validate session token handling at the API gateway level.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestAuthRegistrationLogin:
|
||||
"""Full registration → login → token refresh → profile flow."""
|
||||
|
||||
async def test_full_auth_lifecycle(self, client, db_engine):
|
||||
"""Register → login → get profile → refresh → get profile again."""
|
||||
# Register
|
||||
reg = await client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"email": "lifecycle@example.com",
|
||||
"password": "securepass123",
|
||||
"display_name": "Lifecycle User",
|
||||
},
|
||||
)
|
||||
assert reg.status_code == 201
|
||||
tokens = reg.json()
|
||||
assert "access_token" in tokens
|
||||
assert "refresh_token" in tokens
|
||||
assert tokens["token_type"] == "bearer"
|
||||
assert tokens["expires_in"] > 0
|
||||
|
||||
headers = {"Authorization": f"Bearer {tokens['access_token']}"}
|
||||
|
||||
# Get profile with access token
|
||||
me = await client.get("/auth/me", headers=headers)
|
||||
assert me.status_code == 200
|
||||
assert me.json()["email"] == "lifecycle@example.com"
|
||||
assert me.json()["display_name"] == "Lifecycle User"
|
||||
|
||||
# Sleep 1s so the new token has a different exp than the registration token
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Login with same credentials
|
||||
login = await client.post(
|
||||
"/auth/login",
|
||||
json={"email": "lifecycle@example.com", "password": "securepass123"},
|
||||
)
|
||||
assert login.status_code == 200
|
||||
login_tokens = login.json()
|
||||
assert login_tokens["access_token"] != tokens["access_token"]
|
||||
|
||||
# Refresh token
|
||||
refresh = await client.post(
|
||||
"/auth/refresh",
|
||||
json={"refresh_token": tokens["refresh_token"]},
|
||||
)
|
||||
assert refresh.status_code == 200
|
||||
new_tokens = refresh.json()
|
||||
assert new_tokens["access_token"] != tokens["access_token"]
|
||||
|
||||
# Use refreshed token to access profile
|
||||
new_headers = {"Authorization": f"Bearer {new_tokens['access_token']}"}
|
||||
me2 = await client.get("/auth/me", headers=new_headers)
|
||||
assert me2.status_code == 200
|
||||
assert me2.json()["email"] == "lifecycle@example.com"
|
||||
from tests.conftest import _create_test_user_and_session
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestTokenValidation:
|
||||
"""Token edge cases and error responses."""
|
||||
class TestSessionValidation:
|
||||
"""Session edge cases and error responses."""
|
||||
|
||||
async def test_expired_token_rejected(self, client, db_engine):
|
||||
"""Manually craft an expired token and verify rejection."""
|
||||
import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from jose import jwt
|
||||
|
||||
from cartsnitch_api.config import settings
|
||||
|
||||
payload = {
|
||||
"sub": str(uuid.uuid4()),
|
||||
"exp": datetime.now(UTC) - timedelta(minutes=5),
|
||||
"type": "access",
|
||||
}
|
||||
token = jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)
|
||||
resp = await client.get("/auth/me", headers={"Authorization": f"Bearer {token}"})
|
||||
async def test_invalid_session_token_rejected(self, client, db_engine):
|
||||
resp = await client.get(
|
||||
"/auth/me",
|
||||
headers={"Cookie": "better-auth.session_token=not-a-real-token"},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
async def test_invalid_token_rejected(self, client, db_engine):
|
||||
resp = await client.get("/auth/me", headers={"Authorization": "Bearer not-a-real-token"})
|
||||
assert resp.status_code == 401
|
||||
|
||||
async def test_missing_auth_header(self, client, db_engine):
|
||||
async def test_missing_auth(self, client, db_engine):
|
||||
resp = await client.get("/auth/me")
|
||||
assert resp.status_code in (401, 403)
|
||||
|
||||
async def test_refresh_token_cannot_access_endpoints(self, client, db_engine):
|
||||
"""A refresh token should not work as an access token."""
|
||||
reg = await client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"email": "refresh-test@example.com",
|
||||
"password": "securepass123",
|
||||
"display_name": "Refresh Test",
|
||||
},
|
||||
async def test_bearer_token_also_works(self, client, db_engine):
|
||||
"""Session tokens passed as Bearer tokens should also be accepted."""
|
||||
_, session_token = await _create_test_user_and_session(
|
||||
client, db_engine, email="bearer@e2e.com", display_name="Bearer E2E"
|
||||
)
|
||||
refresh_token = reg.json()["refresh_token"]
|
||||
resp = await client.get("/auth/me", headers={"Authorization": f"Bearer {refresh_token}"})
|
||||
assert resp.status_code == 401
|
||||
|
||||
async def test_deleted_user_token_invalid(self, client, db_engine):
|
||||
"""After deleting an account, tokens should no longer work."""
|
||||
reg = await client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"email": "delete-me@example.com",
|
||||
"password": "securepass123",
|
||||
"display_name": "Delete Me",
|
||||
},
|
||||
resp = await client.get(
|
||||
"/auth/me",
|
||||
headers={"Authorization": f"Bearer {session_token}"},
|
||||
)
|
||||
tokens = reg.json()
|
||||
headers = {"Authorization": f"Bearer {tokens['access_token']}"}
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["email"] == "bearer@e2e.com"
|
||||
|
||||
async def test_deleted_user_session_returns_not_found(self, client, db_engine):
|
||||
"""After deleting a user, their session should result in 404 for profile."""
|
||||
_, session_token = await _create_test_user_and_session(
|
||||
client, db_engine, email="delete-me@e2e.com", display_name="Delete Me"
|
||||
)
|
||||
headers = {"Cookie": f"better-auth.session_token={session_token}"}
|
||||
|
||||
# Delete account
|
||||
delete_resp = await client.delete("/auth/me", headers=headers)
|
||||
assert delete_resp.status_code == 204
|
||||
|
||||
# Profile should fail
|
||||
me = await client.get("/auth/me", headers=headers)
|
||||
assert me.status_code in (401, 404)
|
||||
assert me.status_code == 404
|
||||
|
||||
async def test_expired_session_rejected(self, client, db_engine):
|
||||
"""Expired sessions must be rejected."""
|
||||
import secrets
|
||||
import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
user_id = str(uuid.uuid4())
|
||||
session_token = secrets.token_urlsafe(32)
|
||||
now = datetime.now(UTC).isoformat()
|
||||
expired = (datetime.now(UTC) - timedelta(hours=1)).isoformat()
|
||||
|
||||
async with db_engine.begin() as conn:
|
||||
await conn.execute(
|
||||
text(
|
||||
"INSERT INTO users (id, email, hashed_password, display_name, email_inbound_token, created_at, updated_at) "
|
||||
"VALUES (:id, :email, :hp, :dn, :eit, :ca, :ua)"
|
||||
),
|
||||
{
|
||||
"id": user_id,
|
||||
"email": "expired@e2e.com",
|
||||
"hp": "unused",
|
||||
"dn": "Expired User",
|
||||
"eit": secrets.token_urlsafe(16),
|
||||
"ca": now,
|
||||
"ua": now,
|
||||
},
|
||||
)
|
||||
await conn.execute(
|
||||
text(
|
||||
"INSERT INTO sessions (id, token, user_id, expires_at, created_at, updated_at) "
|
||||
"VALUES (:id, :token, :uid, :ea, :ca, :ua)"
|
||||
),
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"token": session_token,
|
||||
"uid": user_id,
|
||||
"ea": expired,
|
||||
"ca": now,
|
||||
"ua": now,
|
||||
},
|
||||
)
|
||||
|
||||
resp = await client.get(
|
||||
"/auth/me",
|
||||
headers={"Cookie": f"better-auth.session_token={session_token}"},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -154,60 +125,38 @@ class TestAuthProtectedEndpoints:
|
||||
class TestCrossUserDataIsolation:
|
||||
"""Verify that users cannot access other users' data."""
|
||||
|
||||
async def test_user_b_cannot_access_user_a_purchases(self, client, seed_data):
|
||||
"""Register a second user and verify they cannot see User A's purchases."""
|
||||
# User A's purchase (from seed_data)
|
||||
async def test_user_b_cannot_access_user_a_purchases(self, client, db_engine, seed_data):
|
||||
"""A second user cannot see User A's purchases."""
|
||||
purchase_id = str(seed_data["purchases"]["meijer_trip"].id)
|
||||
|
||||
# Register User B
|
||||
reg = await client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"email": "userb@example.com",
|
||||
"password": "securepass123",
|
||||
"display_name": "User B",
|
||||
},
|
||||
_, session_token = await _create_test_user_and_session(
|
||||
client, db_engine, email="userb@e2e.com", display_name="User B"
|
||||
)
|
||||
assert reg.status_code == 201
|
||||
user_b_headers = {"Authorization": f"Bearer {reg.json()['access_token']}"}
|
||||
user_b_headers = {"Cookie": f"better-auth.session_token={session_token}"}
|
||||
|
||||
# User B tries to access User A's specific purchase
|
||||
resp = await client.get(f"/purchases/{purchase_id}", headers=user_b_headers)
|
||||
assert resp.status_code in (403, 404), (
|
||||
"User B should not be able to access User A's purchase"
|
||||
)
|
||||
|
||||
async def test_user_b_purchase_list_is_empty(self, client, seed_data):
|
||||
"""A new user should see no purchases (not User A's purchases)."""
|
||||
reg = await client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"email": "userc@example.com",
|
||||
"password": "securepass123",
|
||||
"display_name": "User C",
|
||||
},
|
||||
async def test_user_b_purchase_list_is_empty(self, client, db_engine, seed_data):
|
||||
"""A new user should see no purchases."""
|
||||
_, session_token = await _create_test_user_and_session(
|
||||
client, db_engine, email="userc@e2e.com", display_name="User C"
|
||||
)
|
||||
assert reg.status_code == 201
|
||||
user_c_headers = {"Authorization": f"Bearer {reg.json()['access_token']}"}
|
||||
user_c_headers = {"Cookie": f"better-auth.session_token={session_token}"}
|
||||
|
||||
resp = await client.get("/purchases", headers=user_c_headers)
|
||||
assert resp.status_code == 200
|
||||
assert len(resp.json()) == 0, "New user should have no purchases"
|
||||
|
||||
async def test_user_b_stores_isolated(self, client, seed_data):
|
||||
async def test_user_b_stores_isolated(self, client, db_engine, seed_data):
|
||||
"""User B's connected stores should be independent from User A."""
|
||||
reg = await client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"email": "userd@example.com",
|
||||
"password": "securepass123",
|
||||
"display_name": "User D",
|
||||
},
|
||||
_, session_token = await _create_test_user_and_session(
|
||||
client, db_engine, email="userd@e2e.com", display_name="User D"
|
||||
)
|
||||
assert reg.status_code == 201
|
||||
user_d_headers = {"Authorization": f"Bearer {reg.json()['access_token']}"}
|
||||
user_d_headers = {"Cookie": f"better-auth.session_token={session_token}"}
|
||||
|
||||
# User D should have no connected stores
|
||||
resp = await client.get("/me/stores", headers=user_d_headers)
|
||||
assert resp.status_code == 200
|
||||
assert len(resp.json()) == 0, "New user should have no connected stores"
|
||||
|
||||
@@ -5,74 +5,6 @@ import pytest
|
||||
from tests.test_e2e.conftest import BAD_UUID, ZERO_UUID
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestRegistrationErrors:
|
||||
"""Validation errors during user registration."""
|
||||
|
||||
async def test_short_password(self, client, db_engine):
|
||||
resp = await client.post(
|
||||
"/auth/register",
|
||||
json={"email": "short@example.com", "password": "short", "display_name": "Test"},
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
async def test_invalid_email(self, client, db_engine):
|
||||
resp = await client.post(
|
||||
"/auth/register",
|
||||
json={"email": "not-an-email", "password": "securepass123", "display_name": "Test"},
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
async def test_missing_fields(self, client, db_engine):
|
||||
resp = await client.post("/auth/register", json={})
|
||||
assert resp.status_code == 422
|
||||
|
||||
async def test_empty_display_name(self, client, db_engine):
|
||||
resp = await client.post(
|
||||
"/auth/register",
|
||||
json={"email": "empty@example.com", "password": "securepass123", "display_name": ""},
|
||||
)
|
||||
assert resp.status_code == 422
|
||||
|
||||
async def test_duplicate_email(self, client, db_engine):
|
||||
payload = {
|
||||
"email": "dupe@example.com",
|
||||
"password": "securepass123",
|
||||
"display_name": "First",
|
||||
}
|
||||
first = await client.post("/auth/register", json=payload)
|
||||
assert first.status_code == 201
|
||||
second = await client.post("/auth/register", json=payload)
|
||||
assert second.status_code == 409
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestLoginErrors:
|
||||
"""Login failure modes."""
|
||||
|
||||
async def test_wrong_password(self, client, db_engine):
|
||||
await client.post(
|
||||
"/auth/register",
|
||||
json={
|
||||
"email": "login-err@example.com",
|
||||
"password": "correctpass1",
|
||||
"display_name": "Login",
|
||||
},
|
||||
)
|
||||
resp = await client.post(
|
||||
"/auth/login",
|
||||
json={"email": "login-err@example.com", "password": "wrongpass123"},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
async def test_nonexistent_user(self, client, db_engine):
|
||||
resp = await client.post(
|
||||
"/auth/login",
|
||||
json={"email": "nobody@example.com", "password": "doesntmatter"},
|
||||
)
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestNotFoundErrors:
|
||||
"""404 responses for missing resources."""
|
||||
|
||||
Reference in New Issue
Block a user