- cache.py:38: Add explicit type annotation for redis.get() return value to resolve mypy no-any-return
- rate_limit.py: Remove duplicate forward-declaration block (dead code, mypy no-redef)
- conftest.py: Remove one excess blank line to satisfy ruff format check
All three fixes verified locally: ruff check ✅, ruff format ✅, mypy ✅
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The hardcoded date(2026, 3, 5) is now > 90 days before
date.today() (2026-06-06), so the default days=90 window
filters it out and the test fails. Use a relative date
(30 days ago) to keep the test green indefinitely.
Add missing blank line between the _set_timestamp_defaults helper
and the next top-level constant so `ruff format --check .` passes.
Pre-existing on dev's HEAD; surfaced after rebasing PR #39 onto dev
in 2b20946.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The conftest was setting rate_limit_redis_enabled=False but the
rate_limit module's _redis_client and the RedisSlidingWindow limiters
are constructed at module import. Flipping the setting inside the
fixture doesn't undo that, so the Redis client was still being
constructed and torn down at the end of the test event loop, raising
RuntimeError('Event loop is closed').
This swaps the limiters directly on the module in the fixture setup
and restores the originals in teardown. Local: 164 passed, 7
skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The rate-limit middleware creates a Redis client at module import time
when rate_limit_redis_enabled is true. The conftest disables
rate_limit_enabled but not the redis flag, so the client still gets
created. After the test event loop closes, the client's async
disconnect raises 'Event loop is closed', surfacing as 500s on
test_validation_error_returns_422_with_field_errors and
test_error_stats_with_valid_key.
Setting rate_limit_redis_enabled=False in the autouse fixture prevents
the Redis client from being created in the first place.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The data routes (purchases, alerts, stores, etc.) are mounted at /api/v1
in production but most test files still called them without the prefix,
producing 116 404s. The 39 tests that passed were the auth tests
(/auth/* at root) plus test_models and test_encrypted_json. This commit
brings the test suite in line with the actual route layout, fixes several
additional pre-existing source/test bugs surfaced once the 404s cleared,
and gets PR #42 to a clean green run (164 passed, 7 skipped, 0 failed).
Source fixes
- src/cartsnitch_api/auth/dependencies.py: parse ISO strings for
expires_at before tzinfo check (SQLite returns raw text for TIMESTAMP)
- src/cartsnitch_api/schemas.py: UserResponse.id is UUID, matching the
actual model type and avoiding ResponseValidationError on /auth/me
Test alignment
- tests/test_routes/*, tests/test_e2e/*: add /api/v1 prefix to all data
route calls (auth routes left alone — they live at root)
- tests/test_openapi.py: refresh EXPECTED_ROUTES to match the actual
OpenAPI spec (drop Better-Auth-only routes, add /api/v1 prefix,
update route count to 31)
Pre-existing test fixes
- tests/test_middleware/test_rate_limit.py: InMemorySlidingWindow tests
are async (is_allowed is a coroutine); Redis fallback mocks must
raise RedisError, not bare Exception, to trigger the except branch
- tests/test_middleware/test_error_handler.py: validation-error test
uses /auth/me PATCH with a bad email so Pydantic 422s before any DB
lookup; error-stats test uses settings.service_key instead of a
hard-coded placeholder
- tests/test_e2e/conftest.py: Coupon.valid_to is date.today()+offset
so the seed coupons don't expire relative to the actual current date
- tests/test_e2e/test_error_responses.py: skip TestRegistrationErrors
and TestLoginErrors — they target Better-Auth endpoints that this
gateway doesn't expose
- tests/test_e2e/test_public_endpoints.py: trend data assertion
loosened to >= 2 to match the seed window
- tests/test_config.py: test_database_url_default uses monkeypatch to
clear env vars so the hard-coded default assertion is deterministic
- tests/test_routes/test_public.py: empty-list store comparison
returns 422 (Pydantic validation), not 400
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three categories of pre-existing CI failure on PR #42:
1. typecheck (mypy src/cartsnitch_api, 9 errors):
- src/cartsnitch_api/config.py:89 — Settings() needs required secret
args that only exist in env at runtime; suppress with
type: ignore[call-arg]
- src/cartsnitch_api/cache.py:38 — redis-py returns Any/bytes,
normalize to str before returning from get()
- src/cartsnitch_api/middleware/rate_limit.py:128,131,134 — three
limiter globals were inferred as RedisSlidingWindow on the if
branch then re-assigned InMemorySlidingWindow on else; declare
them as RateLimitBackend up front
- src/cartsnitch_api/middleware/rate_limit.py:181,187 —
RateLimitBackend Protocol didn't declare max_requests even
though both InMemorySlidingWindow and RedisSlidingWindow expose
it; add max_requests: int to the Protocol
2. test (FK constraint on purchases.user_id):
- tests/conftest.py:_create_test_user_and_session stored user_id
as 32-char hex; test_e2e conftest reads it via raw SQL and wraps
in uuid.UUID (36 chars) before passing to Purchase.user_id, so
the FK never matched. Switch back to str(uuid.uuid4()) (36 chars)
so the stored value and the FK bind value use the same format.
3. Verify lint + format clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Builds on the partial bd6b137 fix (which only stripped server_default
expressions) by also:
- Add _StringUUID TypeDecorator: lets Text/String/UUID columns accept
uuid.UUID values on bind (SQLite has no native UUID type) and returns
uuid.UUID on read so existing test assertions like
isinstance(store.id, uuid.UUID) still pass.
- Replace UUID column types with _StringUUID before create_all so
CREATE TABLE uses CHAR(36) instead of the native UUID type that
SQLite can't bind.
- Extend before_insert listener to also set Text PK columns (User.id)
and func.now()-stripped columns (ingested_at) to Python-side defaults
so INSERTs without explicit values succeed under SQLite.
- Switch _create_test_user_and_session to use 32-char hex user/session
ids so they match the format bound by the TypeDecorator on FK reads.
- Simplify test_encrypted_json.py to use the shared engine/session
fixtures from conftest instead of duplicating its own broken engine.
Tests passing: tests/test_models.py (14), tests/test_encrypted_json.py (6).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add _set_timestamp_defaults event listener to populate created_at/updated_at
before insert when using SQLite, since func.now() server_default is stripped.
Extended server_default stripping to include "now()" expressions for
timestamp columns (created_at, updated_at) that were failing with
NOT NULL constraint errors.
Fixes remaining CI test failures after PR #35:
- NOT NULL constraint failed: stores.created_at
- NOT NULL constraint failed: normalized_products.created_at
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The sync engine fixture (engine) and async engine fixture (db_engine) now
iterate all Base.metadata tables and null server_default on any column
whose SQL text contains 'gen_random_uuid' or 'gen_random_bytes'. This
covers all UUIDPrimaryKeyMixin columns (Purchase, PurchaseItem, Store,
StoreLocation, Coupon, NormalizedProduct, PriceHistory,
ShrinkflationEvent, UserStoreAccount) as well as the
email_inbound_token gen_random_bytes expression in User.
Without this, SQLite raises 'type UUID is not supported' when the ORM
tries to bind Python UUID objects, and NOT NULL constraint failures when
server_default expressions reference non-existent PostgreSQL functions.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Fix test failures: email_inbound_token server_default for SQLite (#29)
Strip PostgreSQL-only server_default from email_inbound_token before SQLite create_all(). Add email_inbound_token to test user INSERT statements.
Reviewed-by: Savannah Savings (CTO)
Approved-by: Checkout Charlie (QA)
The email_inbound_token column uses a PostgreSQL-only server_default
(gen_random_bytes/encode/trim) that SQLite cannot parse.
Strip the server_default before metadata.create_all() in both the
sync engine and async db_engine fixtures so tests run against SQLite.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Auto-fix F401 (unused imports) and I001 (unsorted imports) with ruff --fix
- Manually fix E501 (line too long) in alembic migrations and src/ models
- Run ruff format to ensure consistent formatting
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add rate_limit_auth_requests (5/min) and rate_limit_auth_window_seconds (60) settings
- Add rate_limit_redis_enabled flag for opt-in Redis usage
- Refactor _SlidingWindowCounter into InMemorySlidingWindow class
- Add RedisSlidingWindow using sorted sets with fallback to in-memory
- Add third _auth_strict_limiter for POST /auth/* paths (5 req/min)
- Add protocol-based backend selection at module load time
- Update tests for auth strict limiter and Redis fallback behavior
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add rate_limit_auth_requests (5/min) and rate_limit_auth_window_seconds (60)
settings to config.py
- Refactor rate_limit.py to use protocol/ABC pattern with InMemorySlidingWindow
and RedisSlidingWindow implementations
- Add RedisSlidingWindow using sorted sets for distributed rate limiting
- Add auth_strict_limiter for /auth/* POST endpoints (5 req/min per IP)
- Fall back to in-memory when Redis is unavailable
- Update tests to cover new functionality
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add days query param to GET /public/trends/{product_id} (ge=1, le=365)
- Add category query param to GET /public/store-comparison
- Add category and period query params to GET /public/inflation
- Add boundary and malicious input test cases
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Remove dangerous default values for jwt_secret_key, service_key, and
fernet_key. Add startup validation that raises RuntimeError if these
secrets are not set via environment variables or contain placeholder
values.
Add test fixture to provide explicit test values for these secrets,
ensuring existing tests continue to pass.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Better-Auth sets the session cookie as "token.sessionId". The DB stores
only the token part, so passing the full compound value caused 401s.
Splits on "." for both cookie and Bearer paths.
Tests added for compound cookie, raw token cookie (regression), and
compound Bearer token.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
API config.py now reads CARTSNITCH_DATABASE_URL first, falls back to
DATABASE_URL (which the infra K8s overlay sets for all pods), and finally
falls back to the hardcoded default. Also normalizes plain postgresql://
to postgresql+asyncpg:// for the asyncpg driver.
Fixes CAR-510.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Better-auth v1.5.6 stores raw 32-char tokens in sessions.token, not SHA-256
hashes. The SHA-256 fix from PR #136 causes all authenticated API calls to
return 401 because the UAT sessions table contains raw tokens.
- Remove hashlib from dependencies.py; compare tokens directly
- Remove hashlib from conftest.py; store raw tokens in test DB
- Remove hashlib from test_expired_session_rejected; use raw tokens
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Better-Auth v1.2+ stores SHA-256(raw_token) in the sessions.token
column. The cookie/Bearer header carries the raw token, so the API was
doing a plain-text lookup that would never match a hashed value —
causing all authenticated endpoints to return 401.
- Add hashlib import and hash token in _validate_session_token()
- Update conftest._create_test_user_and_session() to store hashed tokens
- Update test_expired_session_rejected() to store hashed tokens
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Fix email format in AuthService.get_email_in_address to use
receipts+{token}@receipts.cartsnitch.com (was broken: @email.cartsnitch.com)
- Remove dead EmailInAddressResponse class and GET /auth/me/email-in-address
endpoint from auth/routes.py (endpoint moved to routes/user.py)
- Add instructions field to EmailInAddressResponse schema
- Update routes/user.py to include instructions in the response
- Update test URLs from /auth/me/email-in-address to /api/v1/me/email-in-address
Co-authored-by: CartSnitch Engineer Bot <cartnoreply@cartsnitch.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
- Revert auth/dependencies.py to cookie+Bearer dual auth with str user IDs
- Add GET /auth/me/email-in-address endpoint for receipt email routing
- Update User model: add email_inbound_token, change id/store_id/user_id to str
- Update AuthService and UserResponse to use str user IDs
- Update route count test: 33 -> 34 routes
- Restore e2e test for email-in-address endpoint
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- 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>
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>