Compare commits

..

1 Commits

Author SHA1 Message Date
Barcode Betty 4eef2aff92 fix: resolve lint failures blocking CI
CI / lint (pull_request) Failing after 4s
CI / typecheck (pull_request) Failing after 17s
CI / test (pull_request) Failing after 1m34s
CI / build-and-push (pull_request) Has been skipped
CI / deploy-dev (pull_request) Has been skipped
CI / deploy-uat (pull_request) Has been skipped
- src/cartsnitch_api/auth/dependencies.py: remove unused Cookie import
- src/cartsnitch_api/auth/routes.py: remove unused BaseModel, select, and User imports
- src/cartsnitch_api/main.py: fix import ordering

These were pre-existing issues unrelated to CAR-932 fix, blocking CI.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-23 22:19:38 +00:00
20 changed files with 200 additions and 2549 deletions
+108 -4
View File
@@ -15,7 +15,7 @@ permissions:
packages: write packages: write
env: env:
REGISTRY: git.farh.net REGISTRY: ghcr.io
IMAGE_NAME: cartsnitch/api IMAGE_NAME: cartsnitch/api
jobs: jobs:
@@ -51,6 +51,9 @@ jobs:
services: services:
postgres: postgres:
image: postgres:15-alpine image: postgres:15-alpine
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
env: env:
POSTGRES_USER: cartsnitch POSTGRES_USER: cartsnitch
POSTGRES_PASSWORD: cartsnitch_test POSTGRES_PASSWORD: cartsnitch_test
@@ -64,6 +67,9 @@ jobs:
--health-retries 5 --health-retries 5
redis: redis:
image: redis:7-alpine image: redis:7-alpine
credentials:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
ports: ports:
- 6379:6379 - 6379:6379
options: >- options: >-
@@ -116,8 +122,19 @@ jobs:
echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "CalVer tag: $VERSION" echo "CalVer tag: $VERSION"
- name: Log in to Gitea Container Registry - name: Log in to Docker Hub
run: echo "${{ github.token }}" | docker login git.farh.net -u ${{ github.actor }} --password-stdin uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to GHCR
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata - name: Extract metadata
id: meta id: meta
@@ -154,7 +171,7 @@ jobs:
only-fixed: "true" only-fixed: "true"
output-format: sarif output-format: sarif
- name: Push Docker image - name: Push Docker image
if: github.event_name == 'push' if: github.event_name == 'push'
@@ -175,3 +192,90 @@ jobs:
git tag "v${{ steps.calver.outputs.version }}" git tag "v${{ steps.calver.outputs.version }}"
git push origin "v${{ steps.calver.outputs.version }}" git push origin "v${{ steps.calver.outputs.version }}"
deploy-dev:
runs-on: ubuntu-latest
needs: [build-and-push]
if: always() && !cancelled() && github.event_name == 'push' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main')
steps:
- name: Checkout infra repo
uses: actions/checkout@v4
with:
repository: cartsnitch/infra
token: ${{ secrets.GITEA_TOKEN }}
ref: main
path: infra
- name: Install kubectl
uses: azure/setup-kubectl@v4
- name: Install kustomize
uses: imranismail/setup-kustomize@v2
- name: Determine image tag
id: api_tag
run: |
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "tag=${{ needs.build-and-push.outputs.calver_tag }}" >> "$GITHUB_OUTPUT"
else
echo "tag=${{ needs.build-and-push.outputs.sha_tag }}" >> "$GITHUB_OUTPUT"
fi
- name: Update api image tag
if: needs.build-and-push.result == 'success'
run: |
cd infra/apps/overlays/dev
kustomize edit set image ghcr.io/cartsnitch/api:${{ steps.api_tag.outputs.tag }}
- name: Commit and push to infra
run: |
cd infra
git config user.name "cartsnitch-ci[bot]"
git config user.email "cartsnitch-ci[bot]@users.noreply.github.com"
git add apps/overlays/dev/kustomization.yaml
git commit -m "ci(dev): update api image"
git pull --rebase origin main
git push origin main
deploy-uat:
runs-on: ubuntu-latest
needs: [build-and-push]
if: always() && !cancelled() && github.event_name == 'push' && (github.ref == 'refs/heads/uat' || github.ref == 'refs/heads/main')
steps:
- name: Checkout infra repo
uses: actions/checkout@v4
with:
repository: cartsnitch/infra
token: ${{ secrets.GITEA_TOKEN }}
ref: main
path: infra
- name: Install kubectl
uses: azure/setup-kubectl@v4
- name: Install kustomize
uses: imranismail/setup-kustomize@v2
- name: Determine image tag
id: api_tag
run: |
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "tag=${{ needs.build-and-push.outputs.calver_tag }}" >> "$GITHUB_OUTPUT"
else
echo "tag=${{ needs.build-and-push.outputs.sha_tag }}" >> "$GITHUB_OUTPUT"
fi
- name: Update api image tag
if: needs.build-and-push.result == 'success'
run: |
cd infra/apps/overlays/uat
kustomize edit set image ghcr.io/cartsnitch/api:${{ steps.api_tag.outputs.tag }}
- name: Commit and push to infra
run: |
cd infra
git config user.name "cartsnitch-ci[bot]"
git config user.email "cartsnitch-ci[bot]@users.noreply.github.com"
git add apps/overlays/uat/kustomization.yaml
git commit -m "ci(uat): update api image"
git pull --rebase origin main
git push origin main
-11
View File
@@ -1,11 +0,0 @@
{
"mcpServers": {
"gitea": {
"type": "http",
"url": "https://git-mcp.farh.net/mcp",
"headers": {
"Authorization": "Bearer ${GITEA_TOKEN}"
}
}
}
}
+1 -6
View File
@@ -45,11 +45,7 @@ def run_migrations_online() -> None:
poolclass=pool.NullPool, poolclass=pool.NullPool,
) )
with connectable.connect() as connection: with connectable.connect() as connection:
context.configure( context.configure(connection=connection, target_metadata=target_metadata, version_table_column_width=128)
connection=connection,
target_metadata=target_metadata,
version_table_column_width=128,
)
with context.begin_transaction(): with context.begin_transaction():
context.run_migrations() context.run_migrations()
# Create any tables defined in models but not yet created by migrations. # Create any tables defined in models but not yet created by migrations.
@@ -60,7 +56,6 @@ def run_migrations_online() -> None:
connection.commit() connection.commit()
except Exception as exc: except Exception as exc:
import logging import logging
logging.getLogger("alembic.env").warning( logging.getLogger("alembic.env").warning(
"create_all failed (non-fatal, migrations should handle table creation): %s", exc "create_all failed (non-fatal, migrations should handle table creation): %s", exc
) )
+9 -44
View File
@@ -30,10 +30,7 @@ def upgrade() -> None:
if inspector.has_table("users"): if inspector.has_table("users"):
existing_user_cols = [c["name"] for c in inspector.get_columns("users")] existing_user_cols = [c["name"] for c in inspector.get_columns("users")]
if "email_verified" not in existing_user_cols: if "email_verified" not in existing_user_cols:
op.add_column( op.add_column("users", sa.Column("email_verified", sa.Boolean(), nullable=False, server_default="false"))
"users",
sa.Column("email_verified", sa.Boolean(), nullable=False, server_default="false"),
)
if "image" not in existing_user_cols: if "image" not in existing_user_cols:
op.add_column("users", sa.Column("image", sa.Text(), nullable=True)) op.add_column("users", sa.Column("image", sa.Text(), nullable=True))
@@ -47,18 +44,8 @@ def upgrade() -> None:
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("ip_address", sa.Text(), nullable=True), sa.Column("ip_address", sa.Text(), nullable=True),
sa.Column("user_agent", sa.Text(), nullable=True), sa.Column("user_agent", sa.Text(), nullable=True),
sa.Column( sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
"created_at", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
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"), sa.PrimaryKeyConstraint("id"),
) )
op.create_index("ix_sessions_token", "sessions", ["token"], unique=True) op.create_index("ix_sessions_token", "sessions", ["token"], unique=True)
@@ -79,18 +66,8 @@ def upgrade() -> None:
sa.Column("scope", sa.Text(), nullable=True), sa.Column("scope", sa.Text(), nullable=True),
sa.Column("id_token", sa.Text(), nullable=True), sa.Column("id_token", sa.Text(), nullable=True),
sa.Column("password", sa.Text(), nullable=True), sa.Column("password", sa.Text(), nullable=True),
sa.Column( sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
"created_at", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
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"), sa.PrimaryKeyConstraint("id"),
) )
op.create_index("ix_accounts_user_id", "accounts", ["user_id"]) op.create_index("ix_accounts_user_id", "accounts", ["user_id"])
@@ -103,18 +80,8 @@ def upgrade() -> None:
sa.Column("identifier", sa.Text(), nullable=False), sa.Column("identifier", sa.Text(), nullable=False),
sa.Column("value", sa.Text(), nullable=False), sa.Column("value", sa.Text(), nullable=False),
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
sa.Column( sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
"created_at", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
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"), sa.PrimaryKeyConstraint("id"),
) )
@@ -129,10 +96,8 @@ def upgrade() -> None:
user_id_str = str(user_id) user_id_str = str(user_id)
conn.execute( conn.execute(
text( text(
"INSERT INTO accounts " "INSERT INTO accounts (id, user_id, account_id, provider_id, password, created_at, updated_at) "
"(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())"
"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}, {"user_id": user_id_str, "account_id": user_id_str, "password": hashed_password},
) )
+2 -12
View File
@@ -40,12 +40,7 @@ def upgrade() -> None:
return # already TEXT — nothing to do return # already TEXT — nothing to do
# Step 1: Drop existing FK constraints (ignore if they don't exist) # Step 1: Drop existing FK constraints (ignore if they don't exist)
op.execute( op.execute(text("ALTER TABLE user_store_accounts DROP CONSTRAINT IF EXISTS user_store_accounts_user_id_fkey"))
text(
"ALTER TABLE user_store_accounts "
"DROP CONSTRAINT IF EXISTS user_store_accounts_user_id_fkey"
)
)
op.execute(text("ALTER TABLE purchases DROP CONSTRAINT IF EXISTS purchases_user_id_fkey")) op.execute(text("ALTER TABLE purchases DROP CONSTRAINT IF EXISTS purchases_user_id_fkey"))
# Step 2: Alter users.id from uuid to text # Step 2: Alter users.id from uuid to text
@@ -94,12 +89,7 @@ def upgrade() -> None:
def downgrade() -> None: def downgrade() -> None:
# Drop FK constraints # Drop FK constraints
op.execute( op.execute(text("ALTER TABLE user_store_accounts DROP CONSTRAINT IF EXISTS user_store_accounts_user_id_fkey"))
text(
"ALTER TABLE user_store_accounts "
"DROP CONSTRAINT IF EXISTS user_store_accounts_user_id_fkey"
)
)
op.execute(text("ALTER TABLE purchases DROP CONSTRAINT IF EXISTS purchases_user_id_fkey")) op.execute(text("ALTER TABLE purchases DROP CONSTRAINT IF EXISTS purchases_user_id_fkey"))
# Revert users.id from text to uuid # Revert users.id from text to uuid
@@ -20,7 +20,7 @@ depends_on = None
def upgrade() -> None: def upgrade() -> None:
conn = op.get_bind() conn = op.get_bind()
inspector = sa.inspect(conn) inspector = sa.inspect(conn)
# Guard: on fresh DB, Base.metadata.create_all already has the column # Guard: on a fresh DB Base.metadata.create_all creates users table with the column already present
if not inspector.has_table("users"): if not inspector.has_table("users"):
return return
existing_cols = [c["name"] for c in inspector.get_columns("users")] existing_cols = [c["name"] for c in inspector.get_columns("users")]
@@ -6,7 +6,6 @@ Create Date: 2026-04-04
""" """
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op from alembic import op
revision = "006_email_inbound_token_server_default" revision = "006_email_inbound_token_server_default"
@@ -30,8 +29,7 @@ def upgrade() -> None:
"users", "users",
"email_inbound_token", "email_inbound_token",
server_default=sa.text( server_default=sa.text(
"replace(replace(trim(trailing '=' from " "replace(replace(trim(trailing '=' from encode(gen_random_bytes(16), 'base64')), '+', '-'), '/', '_')"
"encode(gen_random_bytes(16), 'base64')), '+', '-'), '/', '_')"
), ),
) )
+3 -13
View File
@@ -27,8 +27,7 @@ def upgrade() -> None:
if inspector.has_table("users"): if inspector.has_table("users"):
return # Table already exists (non-fresh DB or create_all already ran) return # Table already exists (non-fresh DB or create_all already ran)
conn.execute( conn.execute(text("""
text("""
CREATE TABLE users ( CREATE TABLE users (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL UNIQUE,
@@ -37,20 +36,11 @@ def upgrade() -> None:
email_verified BOOLEAN NOT NULL DEFAULT false, email_verified BOOLEAN NOT NULL DEFAULT false,
image TEXT, image TEXT,
email_inbound_token VARCHAR(22) NOT NULL UNIQUE email_inbound_token VARCHAR(22) NOT NULL UNIQUE
DEFAULT ( DEFAULT replace(replace(trim(trailing '=' from encode(gen_random_bytes(16), 'base64')), '+', '-'), '/', '_'),
replace(
replace(
trim(trailing '=' from encode(gen_random_bytes(16), 'base64')),
'+', '-'
),
'/', '_'
)
),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now() updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
) )
""") """))
)
def downgrade() -> None: def downgrade() -> None:
+26 -150
View File
@@ -29,18 +29,8 @@ def upgrade() -> None:
sa.Column("slug", sa.String(20), nullable=False, unique=True), sa.Column("slug", sa.String(20), nullable=False, unique=True),
sa.Column("logo_url", sa.String(500), nullable=True), sa.Column("logo_url", sa.String(500), nullable=True),
sa.Column("website_url", sa.String(500), nullable=True), sa.Column("website_url", sa.String(500), nullable=True),
sa.Column( sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
"created_at", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
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,
),
) )
# 2. store_locations # 2. store_locations
@@ -55,18 +45,8 @@ def upgrade() -> None:
sa.Column("zip", sa.String(10), nullable=False), sa.Column("zip", sa.String(10), nullable=False),
sa.Column("lat", sa.Float(), nullable=True), sa.Column("lat", sa.Float(), nullable=True),
sa.Column("lng", sa.Float(), nullable=True), sa.Column("lng", sa.Float(), nullable=True),
sa.Column( sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
"created_at", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
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,
),
) )
# 3. normalized_products # 3. normalized_products
@@ -81,18 +61,8 @@ def upgrade() -> None:
sa.Column("size", sa.String(50), nullable=True), sa.Column("size", sa.String(50), nullable=True),
sa.Column("size_unit", sa.String(10), nullable=True), sa.Column("size_unit", sa.String(10), nullable=True),
sa.Column("upc_variants", sa.JSON(), nullable=True), sa.Column("upc_variants", sa.JSON(), nullable=True),
sa.Column( sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
"created_at", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
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,
),
) )
# 4. purchases # 4. purchases
@@ -102,9 +72,7 @@ def upgrade() -> None:
sa.Column("id", sa.Uuid(), server_default=text("gen_random_uuid()"), primary_key=True), sa.Column("id", sa.Uuid(), server_default=text("gen_random_uuid()"), primary_key=True),
sa.Column("user_id", sa.Text(), sa.ForeignKey("users.id"), nullable=False), sa.Column("user_id", sa.Text(), sa.ForeignKey("users.id"), nullable=False),
sa.Column("store_id", sa.Uuid(), sa.ForeignKey("stores.id"), nullable=False), sa.Column("store_id", sa.Uuid(), sa.ForeignKey("stores.id"), nullable=False),
sa.Column( sa.Column("store_location_id", sa.Uuid(), sa.ForeignKey("store_locations.id"), nullable=True),
"store_location_id", sa.Uuid(), sa.ForeignKey("store_locations.id"), nullable=True
),
sa.Column("receipt_id", sa.String(200), nullable=False), sa.Column("receipt_id", sa.String(200), nullable=False),
sa.Column("purchase_date", sa.Date(), nullable=False), sa.Column("purchase_date", sa.Date(), nullable=False),
sa.Column("total", sa.Numeric(10, 2), nullable=False), sa.Column("total", sa.Numeric(10, 2), nullable=False),
@@ -113,24 +81,9 @@ def upgrade() -> None:
sa.Column("savings_total", sa.Numeric(10, 2), nullable=True), sa.Column("savings_total", sa.Numeric(10, 2), nullable=True),
sa.Column("source_url", sa.String(500), nullable=True), sa.Column("source_url", sa.String(500), nullable=True),
sa.Column("raw_data", sa.JSON(), nullable=True), sa.Column("raw_data", sa.JSON(), nullable=True),
sa.Column( sa.Column("ingested_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
"ingested_at", sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.DateTime(timezone=True), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
server_default=sa.func.now(),
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.UniqueConstraint("user_id", "store_id", "receipt_id", name="uq_purchase_receipt"), sa.UniqueConstraint("user_id", "store_id", "receipt_id", name="uq_purchase_receipt"),
sa.Index("ix_purchases_user_store", "user_id", "store_id"), sa.Index("ix_purchases_user_store", "user_id", "store_id"),
) )
@@ -151,24 +104,9 @@ def upgrade() -> None:
sa.Column("coupon_discount", sa.Numeric(10, 2), nullable=True), sa.Column("coupon_discount", sa.Numeric(10, 2), nullable=True),
sa.Column("loyalty_discount", sa.Numeric(10, 2), nullable=True), sa.Column("loyalty_discount", sa.Numeric(10, 2), nullable=True),
sa.Column("category_raw", sa.String(100), nullable=True), sa.Column("category_raw", sa.String(100), nullable=True),
sa.Column( sa.Column("normalized_product_id", sa.Uuid(), sa.ForeignKey("normalized_products.id"), nullable=True),
"normalized_product_id", sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Uuid(), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKey("normalized_products.id"),
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,
),
) )
# 6. coupons # 6. coupons
@@ -177,12 +115,7 @@ def upgrade() -> None:
"coupons", "coupons",
sa.Column("id", sa.Uuid(), server_default=text("gen_random_uuid()"), primary_key=True), sa.Column("id", sa.Uuid(), server_default=text("gen_random_uuid()"), primary_key=True),
sa.Column("store_id", sa.Uuid(), sa.ForeignKey("stores.id"), nullable=False), sa.Column("store_id", sa.Uuid(), sa.ForeignKey("stores.id"), nullable=False),
sa.Column( sa.Column("normalized_product_id", sa.Uuid(), sa.ForeignKey("normalized_products.id"), nullable=True),
"normalized_product_id",
sa.Uuid(),
sa.ForeignKey("normalized_products.id"),
nullable=True,
),
sa.Column("title", sa.String(300), nullable=False), sa.Column("title", sa.String(300), nullable=False),
sa.Column("description", sa.String(1000), nullable=True), sa.Column("description", sa.String(1000), nullable=True),
sa.Column("discount_type", sa.String(20), nullable=False), sa.Column("discount_type", sa.String(20), nullable=False),
@@ -194,18 +127,8 @@ def upgrade() -> None:
sa.Column("coupon_code", sa.String(100), nullable=True), sa.Column("coupon_code", sa.String(100), nullable=True),
sa.Column("source_url", sa.String(500), nullable=True), sa.Column("source_url", sa.String(500), nullable=True),
sa.Column("scraped_at", sa.DateTime(timezone=True), nullable=True), sa.Column("scraped_at", sa.DateTime(timezone=True), nullable=True),
sa.Column( sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
"created_at", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
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,
),
) )
# 7. price_history # 7. price_history
@@ -213,12 +136,7 @@ def upgrade() -> None:
op.create_table( op.create_table(
"price_history", "price_history",
sa.Column("id", sa.Uuid(), server_default=text("gen_random_uuid()"), primary_key=True), sa.Column("id", sa.Uuid(), server_default=text("gen_random_uuid()"), primary_key=True),
sa.Column( sa.Column("normalized_product_id", sa.Uuid(), sa.ForeignKey("normalized_products.id"), nullable=False),
"normalized_product_id",
sa.Uuid(),
sa.ForeignKey("normalized_products.id"),
nullable=False,
),
sa.Column("store_id", sa.Uuid(), sa.ForeignKey("stores.id"), nullable=False), sa.Column("store_id", sa.Uuid(), sa.ForeignKey("stores.id"), nullable=False),
sa.Column("observed_date", sa.Date(), nullable=False), sa.Column("observed_date", sa.Date(), nullable=False),
sa.Column("regular_price", sa.Numeric(10, 2), nullable=False), sa.Column("regular_price", sa.Numeric(10, 2), nullable=False),
@@ -226,27 +144,10 @@ def upgrade() -> None:
sa.Column("loyalty_price", sa.Numeric(10, 2), nullable=True), sa.Column("loyalty_price", sa.Numeric(10, 2), nullable=True),
sa.Column("coupon_price", sa.Numeric(10, 2), nullable=True), sa.Column("coupon_price", sa.Numeric(10, 2), nullable=True),
sa.Column("source", sa.String(20), nullable=False), sa.Column("source", sa.String(20), nullable=False),
sa.Column( sa.Column("purchase_item_id", sa.Uuid(), sa.ForeignKey("purchase_items.id"), nullable=True),
"purchase_item_id", sa.Uuid(), sa.ForeignKey("purchase_items.id"), 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.Column( sa.Index("ix_price_history_product_store_date", "normalized_product_id", "store_id", "observed_date"),
"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.Index(
"ix_price_history_product_store_date",
"normalized_product_id",
"store_id",
"observed_date",
),
) )
# 8. shrinkflation_events # 8. shrinkflation_events
@@ -254,12 +155,7 @@ def upgrade() -> None:
op.create_table( op.create_table(
"shrinkflation_events", "shrinkflation_events",
sa.Column("id", sa.Uuid(), server_default=text("gen_random_uuid()"), primary_key=True), sa.Column("id", sa.Uuid(), server_default=text("gen_random_uuid()"), primary_key=True),
sa.Column( sa.Column("normalized_product_id", sa.Uuid(), sa.ForeignKey("normalized_products.id"), nullable=False),
"normalized_product_id",
sa.Uuid(),
sa.ForeignKey("normalized_products.id"),
nullable=False,
),
sa.Column("detected_date", sa.Date(), nullable=False), sa.Column("detected_date", sa.Date(), nullable=False),
sa.Column("old_size", sa.String(50), nullable=False), sa.Column("old_size", sa.String(50), nullable=False),
sa.Column("new_size", sa.String(50), nullable=False), sa.Column("new_size", sa.String(50), nullable=False),
@@ -269,18 +165,8 @@ def upgrade() -> None:
sa.Column("price_at_new_size", sa.Numeric(10, 2), nullable=True), sa.Column("price_at_new_size", sa.Numeric(10, 2), nullable=True),
sa.Column("confidence", sa.Numeric(3, 2), server_default=text("1.00"), nullable=False), sa.Column("confidence", sa.Numeric(3, 2), server_default=text("1.00"), nullable=False),
sa.Column("notes", sa.String(1000), nullable=True), sa.Column("notes", sa.String(1000), nullable=True),
sa.Column( sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
"created_at", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
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,
),
) )
# 9. user_store_accounts # 9. user_store_accounts
@@ -294,18 +180,8 @@ def upgrade() -> None:
sa.Column("session_expires_at", sa.DateTime(timezone=True), nullable=True), sa.Column("session_expires_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_sync_at", sa.DateTime(timezone=True), nullable=True), sa.Column("last_sync_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("status", sa.String(20), server_default=text("'active'"), nullable=False), sa.Column("status", sa.String(20), server_default=text("'active'"), nullable=False),
sa.Column( sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
"created_at", sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
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.UniqueConstraint("user_id", "store_id", name="uq_user_store_account"), sa.UniqueConstraint("user_id", "store_id", name="uq_user_store_account"),
) )
@@ -6,7 +6,6 @@ Create Date: 2026-04-14
""" """
import sqlalchemy as sa import sqlalchemy as sa
from alembic import op from alembic import op
revision = "009_add_gin_index_upc_variants" revision = "009_add_gin_index_upc_variants"
+1 -6
View File
@@ -15,17 +15,12 @@ dependencies = [
"sqlalchemy[asyncio]>=2.0.35", "sqlalchemy[asyncio]>=2.0.35",
"asyncpg>=0.30.0", "asyncpg>=0.30.0",
"alembic>=1.13,<2.0", "alembic>=1.13,<2.0",
"psycopg2-binary>=2.9,<3.0", "psycopg2>=2.9,<3.0",
"python-jose[cryptography]>=3.3.0", "python-jose[cryptography]>=3.3.0",
"passlib[bcrypt]>=1.7.4", "passlib[bcrypt]>=1.7.4",
"httpx>=0.27.0", "httpx>=0.27.0",
"redis[hiredis]>=5.2.0", "redis[hiredis]>=5.2.0",
"cryptography>=43.0.0", "cryptography>=43.0.0",
"pytest[dev]>=9.0.3",
"pytest-asyncio[dev]>=1.3.0",
"aiosqlite[dev]>=0.22.1",
"ruff[dev]>=0.15.14",
"mypy[dev]>=1.19.0",
] ]
[project.optional-dependencies] [project.optional-dependencies]
+1 -2
View File
@@ -6,6 +6,7 @@ from fastapi import APIRouter, FastAPI
from cartsnitch_api.auth.routes import router as auth_router from cartsnitch_api.auth.routes import router as auth_router
from cartsnitch_api.cache import cache_client from cartsnitch_api.cache import cache_client
from cartsnitch_api.database import dispose_engine
from cartsnitch_api.middleware.audit import add_audit_middleware from cartsnitch_api.middleware.audit import add_audit_middleware
from cartsnitch_api.middleware.cors import add_cors_middleware from cartsnitch_api.middleware.cors import add_cors_middleware
from cartsnitch_api.middleware.error_handler import add_error_handlers, add_error_monitor_middleware from cartsnitch_api.middleware.error_handler import add_error_handlers, add_error_monitor_middleware
@@ -25,8 +26,6 @@ from cartsnitch_api.routes.user import router as user_router
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
from cartsnitch_api.database import dispose_engine
await cache_client.initialize() await cache_client.initialize()
yield yield
await cache_client.close() await cache_client.close()
+4 -3
View File
@@ -26,7 +26,9 @@ class User(TimestampMixin, Base):
email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) email: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
hashed_password: Mapped[str | None] = mapped_column(String(255), nullable=True) hashed_password: Mapped[str | None] = mapped_column(String(255), nullable=True)
display_name: Mapped[str | None] = mapped_column(String(100)) display_name: Mapped[str | None] = mapped_column(String(100))
email_verified: Mapped[bool] = mapped_column(Boolean, nullable=False, server_default="false") email_verified: Mapped[bool] = mapped_column(
Boolean, nullable=False, server_default="false"
)
image: Mapped[str | None] = mapped_column(Text, nullable=True) image: Mapped[str | None] = mapped_column(Text, nullable=True)
email_inbound_token: Mapped[str] = mapped_column( email_inbound_token: Mapped[str] = mapped_column(
String(22), String(22),
@@ -34,8 +36,7 @@ class User(TimestampMixin, Base):
unique=True, unique=True,
default=lambda: secrets.token_urlsafe(16), default=lambda: secrets.token_urlsafe(16),
server_default=sa.text( server_default=sa.text(
"replace(replace(trim(trailing '=' from " "replace(replace(trim(trailing '=' from encode(gen_random_bytes(16), 'base64')), '+', '-'), '/', '_')"
"encode(gen_random_bytes(16), 'base64')), '+', '-'), '/', '_')"
), ),
) )
+22 -144
View File
@@ -5,133 +5,20 @@ matching the Better-Auth session validation flow.
""" """
import secrets import secrets
import uuid as uuid_lib import uuid
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
import pytest import pytest
from httpx import ASGITransport, AsyncClient from httpx import ASGITransport, AsyncClient
from sqlalchemy import create_engine, event, text, TypeDecorator, String from sqlalchemy import create_engine, event, text
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID as PostgresUUID
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from sqlalchemy.types import CHAR, TypeEngine
from cartsnitch_api.config import settings as cartsnitch_settings
from cartsnitch_api.database import get_db from cartsnitch_api.database import get_db
from cartsnitch_api.main import create_app from cartsnitch_api.main import create_app
from cartsnitch_api.models import Base from cartsnitch_api.models import Base
cartsnitch_settings = None
class SQLiteCompatibleUUID(TypeDecorator):
"""Adapts PostgreSQL UUID for use with SQLite at test runtime.
Stores as CHAR(32) hex string, converts to Python uuid.UUID on read.
"""
impl = CHAR(32)
cache_ok = True
def process_bind_param(self, value, dialect):
if value is None:
return None
if isinstance(value, uuid_lib.UUID):
return value.hex
return str(value)
def process_result_value(self, value, dialect):
if value is None:
return None
return uuid_lib.UUID(hex=value)
class _StringUUID(TypeDecorator):
"""Fallback TypeDecorator that accepts both UUID objects and strings.
Used when the model uses Text but tests pass uuid.UUID values.
Stores as native string, returns uuid.UUID on read.
"""
impl = String(36)
cache_ok = True
def process_bind_param(self, value, dialect):
if value is None:
return None
if isinstance(value, uuid_lib.UUID):
return value.hex
return str(value)
def process_result_value(self, value, dialect):
if value is None:
return None
try:
return uuid_lib.UUID(hex=value)
except Exception:
return value
def _adapt_uuid_columns_for_sqlite(googol=None):
"""Replace PostgreSQL UUID column types with SQLiteCompatibleUUID.
PostgreSQL UUID columns generate DDL using gen_random_uuid() which SQLite
doesn't support. This replaces those column types so SQLite can bind UUIDs
as hex strings. Also sets a Python-side default so INSERTs without explicit
id succeed. Accepts an optional connection arg from run_sync.
"""
for table in Base.metadata.tables.values():
for column in table.columns.values():
if isinstance(column.type, PostgresUUID):
column.type = SQLiteCompatibleUUID()
if column.server_default is None and column.default is None:
column.default = uuid_lib.uuid4
def _adapt_text_pk_columns_for_uuid(googol=None):
"""Replace Text primary key columns that tests bind uuid.UUID values into.
User.id is a Text column but tests pass uuid.UUID objects. SQLite can't
bind UUID directly so we swap these to a compatible type. Accepts an
optional connection arg from run_sync.
"""
for table in Base.metadata.tables.values():
for column in table.columns.values():
if column.primary_key and isinstance(column.type, sa.Text):
column.type = _StringUUID()
def _adapt_fk_columns_for_uuid(googol=None):
"""Replace FK columns that reference UUID PKs but are typed as Text.
purchase.user_id, user_store_accounts.user_id/store_id, etc. are typed as str
but tests pass uuid.UUID objects for them. Accepts an optional connection arg
from run_sync.
"""
for table in Base.metadata.tables.values():
for column in table.columns.values():
if column.foreign_keys:
for fk in column.foreign_keys:
if isinstance(column.type, (sa.Text, sa.String)) and column.type.length in (None, 255):
column.type = _StringUUID()
def _strip_postgres_server_defaults(googol=None):
"""Remove PostgreSQL-specific server_default expressions for SQLite compatibility.
PostgreSQL functions like gen_random_uuid() and the base64 encoding expression
in email_inbound_token are not valid in SQLite. Accepts an optional connection
arg from run_sync.
"""
for table in Base.metadata.tables.values():
for column in table.columns.values():
if column.server_default is not None:
sd = str(column.server_default.arg)
if "gen_random_uuid" in sd or "gen_random_bytes" in sd:
column.server_default = None
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
TEST_JWT_SECRET = secrets.token_urlsafe(32) TEST_JWT_SECRET = secrets.token_urlsafe(32)
TEST_SERVICE_KEY = secrets.token_urlsafe(32) TEST_SERVICE_KEY = secrets.token_urlsafe(32)
TEST_FERNET_KEY = "7reF42nmTwbdN21PBoubGp7h_FU8qSimstmlaMLoRK8=" TEST_FERNET_KEY = "7reF42nmTwbdN21PBoubGp7h_FU8qSimstmlaMLoRK8="
@@ -139,35 +26,33 @@ TEST_FERNET_KEY = "7reF42nmTwbdN21PBoubGp7h_FU8qSimstmlaMLoRK8="
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def setup_test_settings(): def setup_test_settings():
from cartsnitch_api.config import settings as real_settings original_jwt = cartsnitch_settings.jwt_secret_key
original_jwt = real_settings.jwt_secret_key original_service = cartsnitch_settings.service_key
original_service = real_settings.service_key original_fernet = cartsnitch_settings.fernet_key
original_fernet = real_settings.fernet_key cartsnitch_settings.jwt_secret_key = TEST_JWT_SECRET
real_settings.jwt_secret_key = TEST_JWT_SECRET cartsnitch_settings.service_key = TEST_SERVICE_KEY
real_settings.service_key = TEST_SERVICE_KEY cartsnitch_settings.fernet_key = TEST_FERNET_KEY
real_settings.fernet_key = TEST_FERNET_KEY
yield yield
real_settings.jwt_secret_key = original_jwt cartsnitch_settings.jwt_secret_key = original_jwt
real_settings.service_key = original_service cartsnitch_settings.service_key = original_service
real_settings.fernet_key = original_fernet cartsnitch_settings.fernet_key = original_fernet
TEST_DATABASE_URL = "sqlite+aiosqlite:///:memory:"
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def disable_rate_limiting(): def disable_rate_limiting():
from cartsnitch_api.config import settings as real_settings """Disable rate limiting for all tests to prevent 429 interference."""
real_settings.rate_limit_enabled = False cartsnitch_settings.rate_limit_enabled = False
yield yield
real_settings.rate_limit_enabled = True cartsnitch_settings.rate_limit_enabled = True
@pytest.fixture @pytest.fixture
def engine(): def engine():
"""Sync in-memory SQLite engine for model unit tests.""" """Sync in-memory SQLite engine for model unit tests."""
eng = create_engine("sqlite:///:memory:") eng = create_engine("sqlite:///:memory:")
_adapt_uuid_columns_for_sqlite()
_adapt_text_pk_columns_for_uuid()
_adapt_fk_columns_for_uuid()
_strip_postgres_server_defaults()
Base.metadata.create_all(eng) Base.metadata.create_all(eng)
yield eng yield eng
eng.dispose() eng.dispose()
@@ -192,10 +77,6 @@ async def db_engine():
cursor.close() cursor.close()
async with engine.begin() as conn: async with engine.begin() as conn:
await conn.run_sync(_adapt_uuid_columns_for_sqlite)
await conn.run_sync(_adapt_text_pk_columns_for_uuid)
await conn.run_sync(_adapt_fk_columns_for_uuid)
await conn.run_sync(_strip_postgres_server_defaults)
await conn.run_sync(Base.metadata.create_all) await conn.run_sync(Base.metadata.create_all)
# Create Better-Auth tables (not managed by SQLAlchemy models) # Create Better-Auth tables (not managed by SQLAlchemy models)
await conn.execute( await conn.execute(
@@ -285,21 +166,19 @@ async def _create_test_user_and_session(
Returns (user_dict, session_token). Better-Auth stores the raw token Returns (user_dict, session_token). Better-Auth stores the raw token
in the DB, so we insert it as-is. in the DB, so we insert it as-is.
""" """
user_id = str(uuid_lib.uuid4()) user_id = str(uuid.uuid4())
email = user_overrides.get("email", "test@example.com") email = user_overrides.get("email", "test@example.com")
display_name = user_overrides.get("display_name", "Test User") display_name = user_overrides.get("display_name", "Test User")
session_token = secrets.token_urlsafe(32) session_token = secrets.token_urlsafe(32)
session_id = str(uuid_lib.uuid4()) session_id = str(uuid.uuid4())
now = datetime.now(UTC).isoformat() now = datetime.now(UTC).isoformat()
expires = (datetime.now(UTC) + timedelta(days=7)).isoformat() expires = (datetime.now(UTC) + timedelta(days=7)).isoformat()
async with db_engine.begin() as conn: async with db_engine.begin() as conn:
await conn.execute( await conn.execute(
text( text(
"INSERT INTO users (id, email, hashed_password, display_name, " "INSERT INTO users (id, email, hashed_password, display_name, email_verified, created_at, updated_at) "
"email_verified, email_inbound_token, created_at, updated_at) " "VALUES (:id, :email, :hashed_password, :display_name, :email_verified, :created_at, :updated_at)"
"VALUES (:id, :email, :hashed_password, :display_name, "
":email_verified, :email_inbound_token, :created_at, :updated_at)"
), ),
{ {
"id": user_id, "id": user_id,
@@ -307,7 +186,6 @@ async def _create_test_user_and_session(
"hashed_password": "not-used-with-better-auth", "hashed_password": "not-used-with-better-auth",
"display_name": display_name, "display_name": display_name,
"email_verified": False, "email_verified": False,
"email_inbound_token": secrets.token_urlsafe(16),
"created_at": now, "created_at": now,
"updated_at": now, "updated_at": now,
}, },
+2 -4
View File
@@ -138,9 +138,8 @@ async def test_expired_session_rejected(client, db_engine):
async with db_engine.begin() as conn: async with db_engine.begin() as conn:
await conn.execute( await conn.execute(
text( text(
"INSERT INTO users (id, email, hashed_password, display_name, " "INSERT INTO users (id, email, hashed_password, display_name, email_verified, created_at, updated_at) "
"email_verified, email_inbound_token, created_at, updated_at) " "VALUES (:id, :email, :hp, :dn, :ev, :ca, :ua)"
"VALUES (:id, :email, :hp, :dn, :ev, :token, :ca, :ua)"
), ),
{ {
"id": user_id, "id": user_id,
@@ -148,7 +147,6 @@ async def test_expired_session_rejected(client, db_engine):
"hp": "unused", "hp": "unused",
"dn": "Expired User", "dn": "Expired User",
"ev": False, "ev": False,
"token": secrets.token_urlsafe(16),
"ca": now, "ca": now,
"ua": now, "ua": now,
}, },
+5 -12
View File
@@ -1,5 +1,7 @@
"""Tests for Settings config, specifically the database_url env var fallback.""" """Tests for Settings config, specifically the database_url env var fallback."""
import os
from cartsnitch_api.config import Settings from cartsnitch_api.config import Settings
@@ -28,10 +30,7 @@ def test_database_url_normalizes_plain_postgresql_prefix():
"DATABASE_URL": "postgresql://cartsnitch:cartsnitch@localhost:5432/cartsnitch", "DATABASE_URL": "postgresql://cartsnitch:cartsnitch@localhost:5432/cartsnitch",
} }
settings = Settings(**env) settings = Settings(**env)
assert ( assert settings.database_url == "postgresql+asyncpg://cartsnitch:cartsnitch@localhost:5432/cartsnitch"
settings.database_url
== "postgresql+asyncpg://cartsnitch:cartsnitch@localhost:5432/cartsnitch"
)
def test_database_url_preserves_asyncpg_prefix(): def test_database_url_preserves_asyncpg_prefix():
@@ -40,16 +39,10 @@ def test_database_url_preserves_asyncpg_prefix():
"CARTSNITCH_DATABASE_URL": "postgresql+asyncpg://cartsnitch:cartsnitch@localhost:5432/cartsnitch", "CARTSNITCH_DATABASE_URL": "postgresql+asyncpg://cartsnitch:cartsnitch@localhost:5432/cartsnitch",
} }
settings = Settings(**env) settings = Settings(**env)
assert ( assert settings.database_url == "postgresql+asyncpg://cartsnitch:cartsnitch@localhost:5432/cartsnitch"
settings.database_url
== "postgresql+asyncpg://cartsnitch:cartsnitch@localhost:5432/cartsnitch"
)
def test_database_url_default(): def test_database_url_default():
"""When neither env var is set, the hardcoded default is used.""" """When neither env var is set, the hardcoded default is used."""
settings = Settings() settings = Settings()
assert ( assert settings.database_url == "postgresql+asyncpg://cartsnitch:cartsnitch@localhost:5432/cartsnitch"
settings.database_url
== "postgresql+asyncpg://cartsnitch:cartsnitch@localhost:5432/cartsnitch"
)
+2 -4
View File
@@ -65,9 +65,8 @@ class TestSessionValidation:
async with db_engine.begin() as conn: async with db_engine.begin() as conn:
await conn.execute( await conn.execute(
text( text(
"INSERT INTO users (id, email, hashed_password, display_name, " "INSERT INTO users (id, email, hashed_password, display_name, email_verified, created_at, updated_at) "
"email_verified, email_inbound_token, created_at, updated_at) " "VALUES (:id, :email, :hp, :dn, :ev, :ca, :ua)"
"VALUES (:id, :email, :hp, :dn, :ev, :token, :ca, :ua)"
), ),
{ {
"id": user_id, "id": user_id,
@@ -75,7 +74,6 @@ class TestSessionValidation:
"hp": "unused", "hp": "unused",
"dn": "Expired User", "dn": "Expired User",
"ev": False, "ev": False,
"token": secrets.token_urlsafe(16),
"ca": now, "ca": now,
"ua": now, "ua": now,
}, },
+11 -7
View File
@@ -1,12 +1,11 @@
"""Tests for EncryptedJSON TypeDecorator and session_data encryption.""" """Tests for EncryptedJSON TypeDecorator and session_data encryption."""
import json import json
import uuid as uuid_lib
import pytest import pytest
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from pydantic import ValidationError from pydantic import ValidationError
from sqlalchemy import column, table, text from sqlalchemy import column, create_engine, table, text
from sqlalchemy.orm import sessionmaker from sqlalchemy.orm import sessionmaker
from cartsnitch_api.config import settings from cartsnitch_api.config import settings
@@ -16,13 +15,18 @@ from cartsnitch_api.models.user import User, UserStoreAccount
@pytest.fixture @pytest.fixture
def engine(engine): def engine():
yield engine eng = create_engine("sqlite:///:memory:")
Base.metadata.create_all(eng)
yield eng
eng.dispose()
@pytest.fixture @pytest.fixture
def session(session): def session(engine):
yield session factory = sessionmaker(bind=engine)
with factory() as sess:
yield sess
@pytest.fixture @pytest.fixture
@@ -36,7 +40,7 @@ def store(session):
@pytest.fixture @pytest.fixture
def user(session): def user(session):
u = User(id=uuid_lib.uuid4(), email="alice@example.com", hashed_password="fakehash") u = User(email="alice@example.com", hashed_password="fakehash")
session.add(u) session.add(u)
session.commit() session.commit()
session.refresh(u) session.refresh(u)
+1 -1
View File
@@ -1,7 +1,7 @@
"""Tests for rate limiting middleware.""" """Tests for rate limiting middleware."""
import time import time
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock, patch
import pytest import pytest
Generated
-2121
View File
File diff suppressed because it is too large Load Diff