forked from cartsnitch/cartsnitch
feat: add dedicated UAT seed user with known credentials
Add guaranteed UAT test user (uat@cartsnitch.com / CartSnitch-UAT-2026!) seeded via Better-Auth bcrypt path. Idempotent — re-running the seed skips the user if it already exists. - Add 002_better_auth_tables Alembic migration (sessions, accounts, verifications tables + email_verified/image on users) - Add bcrypt>=4.0,<6.0 to [seed] extra (CTO feedback: was bcrypt>=0.15,<1.0 which matches zero installable versions) - Fix account_id to use str(UAT_USER_ID) to match migration convention (CTO feedback: was using UAT_EMAIL which was inconsistent) - Document credentials in common/README.md under Test Users Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,101 @@
|
|||||||
|
"""Add Better-Auth tables and extend users table.
|
||||||
|
|
||||||
|
Creates sessions, accounts, and verifications tables for Better-Auth.
|
||||||
|
Adds email_verified and image columns to existing users table.
|
||||||
|
Migrates password hashes from users.hashed_password to accounts.password.
|
||||||
|
|
||||||
|
Revision ID: 002_better_auth_tables
|
||||||
|
Revises: 001_encrypt_session_data
|
||||||
|
Create Date: 2026-03-28
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision = "002_better_auth_tables"
|
||||||
|
down_revision = "001_encrypt_session_data"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# --- Extend users table for Better-Auth compatibility ---
|
||||||
|
op.add_column("users", sa.Column("email_verified", sa.Boolean(), nullable=False, server_default="false"))
|
||||||
|
op.add_column("users", sa.Column("image", sa.Text(), nullable=True))
|
||||||
|
|
||||||
|
# --- Create sessions table ---
|
||||||
|
op.create_table(
|
||||||
|
"sessions",
|
||||||
|
sa.Column("id", sa.Text(), nullable=False),
|
||||||
|
sa.Column("token", sa.Text(), nullable=False),
|
||||||
|
sa.Column("user_id", sa.Text(), nullable=False),
|
||||||
|
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column("ip_address", sa.Text(), nullable=True),
|
||||||
|
sa.Column("user_agent", sa.Text(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||||
|
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index("ix_sessions_token", "sessions", ["token"], unique=True)
|
||||||
|
op.create_index("ix_sessions_user_id", "sessions", ["user_id"])
|
||||||
|
|
||||||
|
# --- Create accounts table ---
|
||||||
|
op.create_table(
|
||||||
|
"accounts",
|
||||||
|
sa.Column("id", sa.Text(), nullable=False),
|
||||||
|
sa.Column("user_id", sa.Text(), nullable=False),
|
||||||
|
sa.Column("account_id", sa.Text(), nullable=False),
|
||||||
|
sa.Column("provider_id", sa.Text(), nullable=False),
|
||||||
|
sa.Column("access_token", sa.Text(), nullable=True),
|
||||||
|
sa.Column("refresh_token", sa.Text(), nullable=True),
|
||||||
|
sa.Column("access_token_expires_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column("refresh_token_expires_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column("scope", sa.Text(), nullable=True),
|
||||||
|
sa.Column("id_token", sa.Text(), nullable=True),
|
||||||
|
sa.Column("password", sa.Text(), nullable=True),
|
||||||
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||||
|
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index("ix_accounts_user_id", "accounts", ["user_id"])
|
||||||
|
|
||||||
|
# --- Create verifications table ---
|
||||||
|
op.create_table(
|
||||||
|
"verifications",
|
||||||
|
sa.Column("id", sa.Text(), nullable=False),
|
||||||
|
sa.Column("identifier", sa.Text(), nullable=False),
|
||||||
|
sa.Column("value", sa.Text(), nullable=False),
|
||||||
|
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||||
|
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- Migrate existing password hashes to accounts table ---
|
||||||
|
# For each user with a hashed_password, create a 'credential' account row
|
||||||
|
conn = op.get_bind()
|
||||||
|
users = conn.execute(
|
||||||
|
text("SELECT id, hashed_password FROM users WHERE hashed_password IS NOT NULL")
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
for user_id, hashed_password in users:
|
||||||
|
user_id_str = str(user_id)
|
||||||
|
conn.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": user_id_str, "account_id": user_id_str, "password": hashed_password},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table("verifications")
|
||||||
|
op.drop_table("accounts")
|
||||||
|
op.drop_index("ix_sessions_user_id", table_name="sessions")
|
||||||
|
op.drop_index("ix_sessions_token", table_name="sessions")
|
||||||
|
op.drop_table("sessions")
|
||||||
|
op.drop_column("users", "image")
|
||||||
|
op.drop_column("users", "email_verified")
|
||||||
@@ -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)
|
||||||
@@ -27,6 +27,7 @@ dev = [
|
|||||||
]
|
]
|
||||||
seed = [
|
seed = [
|
||||||
"faker>=33.0,<34.0",
|
"faker>=33.0,<34.0",
|
||||||
|
"bcrypt>=4.0,<6.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
import random
|
import random
|
||||||
import time
|
import time
|
||||||
|
import uuid
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
from faker import Faker
|
from faker import Faker
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -184,6 +186,65 @@ def run_seed(
|
|||||||
|
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
_seed_uat_user(session)
|
||||||
|
|
||||||
elapsed = time.monotonic() - t0
|
elapsed = time.monotonic() - t0
|
||||||
_log("")
|
_log("")
|
||||||
_log(f"Seed complete in {elapsed:.1f}s")
|
_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")
|
||||||
|
|||||||
Reference in New Issue
Block a user