diff --git a/common/README.md b/common/README.md new file mode 100644 index 0000000..75ff58a --- /dev/null +++ b/common/README.md @@ -0,0 +1,28 @@ +# CartSnitch Common + +Shared models, schemas, and utilities for CartSnitch services. + +## Test Users + +The following users are seeded by `cartsnitch-seed` and can be used for local development and UAT. + +| Email | Password | Display Name | Notes | +|---|---|---|---| +| `uat@cartsnitch.com` | `CartSnitch-UAT-2026!` | UAT Tester | Primary UAT account. Use for regression testing in the CartSnitch frontend. Created by the seed runner via Better-Auth's bcrypt path — credentials work against the live auth service. Idempotent; re-running the seed skips this user if it already exists. | + +### Running the Seed + +```bash +# Install with seed dependencies +pip install -e "cartsnitch-common[seed]" + +# Run (requires CARTSNITCH_DATABASE_URL_SYNC) +CARTSNITCH_DATABASE_URL_SYNC=postgresql://user:pass@localhost:5432/cartsnitch \ + cartsnitch-seed +``` + +### Architecture + +- **Models** live in `src/cartsnitch_common/models/` +- **Alembic migrations** run via the `api` service (`api/alembic/`) +- **Seed runner** runs via `cartsnitch-seed` (installed as a package entry point) diff --git a/common/pyproject.toml b/common/pyproject.toml index ee348c5..5aa0309 100644 --- a/common/pyproject.toml +++ b/common/pyproject.toml @@ -27,6 +27,7 @@ dev = [ ] seed = [ "faker>=33.0,<34.0", + "bcrypt>=4.0,<6.0", ] [project.scripts] diff --git a/common/src/cartsnitch_common/seed/runner.py b/common/src/cartsnitch_common/seed/runner.py index c2b7784..d804b28 100644 --- a/common/src/cartsnitch_common/seed/runner.py +++ b/common/src/cartsnitch_common/seed/runner.py @@ -2,8 +2,10 @@ import random import time +import uuid from typing import Any +import bcrypt from faker import Faker from sqlalchemy import text from sqlalchemy.orm import Session @@ -184,6 +186,65 @@ def run_seed( session.commit() + _seed_uat_user(session) + elapsed = time.monotonic() - t0 _log("") _log(f"Seed complete in {elapsed:.1f}s") + + +# --------------------------------------------------------------------------- +# UAT seed user +# --------------------------------------------------------------------------- + +UAT_EMAIL = "uat@cartsnitch.com" +UAT_PASSWORD = "CartSnitch-UAT-2026!" +UAT_DISPLAY_NAME = "UAT Tester" +UAT_USER_ID = uuid.UUID("00000000-0000-0000-0000-000000000001") + + +def _seed_uat_user(session: Session) -> None: + """Insert or verify the dedicated UAT test user. + + The user is created via Better-Auth's bcrypt hashing path so credentials + work against the live auth service. Idempotent — skips if the user already + exists. + """ + existing = session.execute( + text("SELECT id FROM users WHERE email = :email"), + {"email": UAT_EMAIL}, + ).fetchone() + + if existing is not None: + _log(f"UAT user {UAT_EMAIL} already exists — skipping") + return + + password_hash = bcrypt.hashpw(UAT_PASSWORD.encode(), bcrypt.gensalt()).decode() + + session.execute( + text( + "INSERT INTO users (id, email, hashed_password, display_name, email_verified, created_at, updated_at) " + "VALUES (:id, :email, :hashed_password, :display_name, true, now(), now())" + ), + { + "id": str(UAT_USER_ID), + "email": UAT_EMAIL, + "hashed_password": password_hash, + "display_name": UAT_DISPLAY_NAME, + }, + ) + + session.execute( + text( + "INSERT INTO accounts (id, user_id, account_id, provider_id, password, created_at, updated_at) " + "VALUES (gen_random_uuid()::text, :user_id, :account_id, 'credential', :password, now(), now())" + ), + { + "user_id": str(UAT_USER_ID), + "account_id": str(UAT_USER_ID), + "password": password_hash, + }, + ) + + session.commit() + _log(f"UAT user {UAT_EMAIL} created")