Files
Coupon Carl 782448a54a feat: migrate authentication to Better-Auth (Phase 1)
Replace hand-rolled JWT auth with Better-Auth session-based authentication.

- Scaffold auth/ Node.js service with Better-Auth, bcrypt password compat,
  Postgres adapter mapped to existing users table
- Add Alembic migration (002) creating sessions, accounts, verifications
  tables and migrating password hashes to accounts table
- Update FastAPI auth dependency to validate sessions via shared DB
  (supports both cookie and Bearer token)
- Remove registration/login/refresh endpoints from API gateway (now
  handled by Better-Auth service)
- Update frontend to use better-auth/react client with httpOnly cookies
  (no tokens in localStorage or memory)
- Rewrite auth store, Login, Register, Dashboard, Settings, ProtectedRoute
  to use session-based auth
- Update all tests to create sessions directly in DB instead of JWT tokens

Resolves CAR-27
See plan: CAR-26#document-plan

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-28 04:46:10 +00:00

115 lines
3.6 KiB
Python

"""Integration tests for purchase endpoints."""
import secrets
import uuid
from datetime import UTC, date, datetime, timedelta
from decimal import Decimal
import pytest
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from cartsnitch_api.models import Purchase, PurchaseItem, Store, User
@pytest.fixture
async def purchase_data(db_engine):
"""Seed a user, store, purchase, items, and a valid session."""
factory = async_sessionmaker(db_engine, class_=AsyncSession, expire_on_commit=False)
async with factory() as session:
user = User(
email="buyer@example.com",
hashed_password="not-used-with-better-auth",
display_name="Buyer",
)
store = Store(name="Kroger", slug="kroger")
session.add_all([user, store])
await session.commit()
await session.refresh(user)
await session.refresh(store)
purchase = Purchase(
user_id=user.id,
store_id=store.id,
receipt_id="receipt-001",
purchase_date=date(2026, 3, 10),
total=Decimal("42.50"),
)
session.add(purchase)
await session.commit()
await session.refresh(purchase)
item = PurchaseItem(
purchase_id=purchase.id,
product_name_raw="Organic Milk 1gal",
quantity=Decimal("1"),
unit_price=Decimal("5.99"),
extended_price=Decimal("5.99"),
)
session.add(item)
await session.commit()
# Create a session token directly in the sessions table
session_token = secrets.token_urlsafe(32)
now = datetime.now(UTC).isoformat()
expires = (datetime.now(UTC) + timedelta(days=7)).isoformat()
async with db_engine.begin() as conn:
await conn.execute(
text(
"INSERT INTO sessions (id, token, user_id, expires_at, created_at, updated_at) "
"VALUES (:id, :token, :user_id, :expires_at, :created_at, :updated_at)"
),
{
"id": str(uuid.uuid4()),
"token": session_token,
"user_id": str(user.id),
"expires_at": expires,
"created_at": now,
"updated_at": now,
},
)
return {
"user": user,
"store": store,
"purchase": purchase,
"headers": {"Cookie": f"better-auth.session_token={session_token}"},
}
@pytest.mark.asyncio
async def test_list_purchases(client, purchase_data):
resp = await client.get("/purchases", headers=purchase_data["headers"])
assert resp.status_code == 200
data = resp.json()
assert len(data) == 1
assert data[0]["store_name"] == "Kroger"
assert data[0]["total"] == 42.50
@pytest.mark.asyncio
async def test_get_purchase_detail(client, purchase_data):
pid = str(purchase_data["purchase"].id)
resp = await client.get(f"/purchases/{pid}", headers=purchase_data["headers"])
assert resp.status_code == 200
data = resp.json()
assert len(data["line_items"]) == 1
assert data["line_items"][0]["name"] == "Organic Milk 1gal"
@pytest.mark.asyncio
async def test_get_purchase_not_found(client, auth_headers):
resp = await client.get(f"/purchases/{uuid.uuid4()}", headers=auth_headers)
assert resp.status_code == 404
@pytest.mark.asyncio
async def test_purchase_stats(client, purchase_data):
resp = await client.get("/purchases/stats", headers=purchase_data["headers"])
assert resp.status_code == 200
data = resp.json()
assert data["total_spent"] == 42.50
assert data["purchase_count"] == 1
assert "Kroger" in data["by_store"]