From 6755ca8c2709e02f946fd313461713213a37f01b Mon Sep 17 00:00:00 2001 From: Barcode Betty Date: Sat, 23 May 2026 23:36:08 +0000 Subject: [PATCH 1/7] Fix: strip PostgreSQL server_default from UUID + gen_random_bytes columns for SQLite tests 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 --- tests/conftest.py | 31 ++++++++++++++++++++++--------- tests/test_encrypted_json.py | 9 +++++++++ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5bd4e67..6439552 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,12 +51,21 @@ def disable_rate_limiting(): @pytest.fixture def engine(): - """Sync in-memory SQLite engine for model unit tests.""" - eng = create_engine("sqlite:///:memory:") - from cartsnitch_api.models.user import User + """Sync in-memory SQLite engine for model unit tests. + + Strips PostgreSQL-specific server_default expressions so SQLite can + handle all column inserts without missing-function errors. + """ + eng = create_engine("sqlite:///:memory:") + + for table in Base.metadata.tables.values(): + for col in table.columns.values(): + sd = col.server_default + if sd is not None: + expr_str = str(sd.expression).lower() + if "gen_random_uuid" in expr_str or "gen_random_bytes" in expr_str: + col.server_default = None - col = User.__table__.columns["email_inbound_token"] - col.server_default = None Base.metadata.create_all(eng) yield eng eng.dispose() @@ -80,12 +89,16 @@ async def db_engine(): cursor.execute("PRAGMA foreign_keys=ON") cursor.close() - async with engine.begin() as conn: - from cartsnitch_api.models.user import User + for table in Base.metadata.tables.values(): + for col in table.columns.values(): + sd = col.server_default + if sd is not None: + expr_str = str(sd.expression).lower() + if "gen_random_uuid" in expr_str or "gen_random_bytes" in expr_str: + col.server_default = None - User.__table__.columns["email_inbound_token"].server_default = None + async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) - # Create Better-Auth tables (not managed by SQLAlchemy models) await conn.execute( text(""" CREATE TABLE IF NOT EXISTS sessions ( diff --git a/tests/test_encrypted_json.py b/tests/test_encrypted_json.py index 2ef3ccb..07cf44c 100644 --- a/tests/test_encrypted_json.py +++ b/tests/test_encrypted_json.py @@ -17,6 +17,15 @@ from cartsnitch_api.models.user import User, UserStoreAccount @pytest.fixture def engine(): eng = create_engine("sqlite:///:memory:") + + for table in Base.metadata.tables.values(): + for col in table.columns.values(): + sd = col.server_default + if sd is not None: + expr_str = str(sd.expression).lower() + if "gen_random_uuid" in expr_str or "gen_random_bytes" in expr_str: + col.server_default = None + Base.metadata.create_all(eng) yield eng eng.dispose() -- 2.52.0 From 84c143c4e737882d624aea091129457e63df83de Mon Sep 17 00:00:00 2001 From: Barcode Betty <32+cs_betty@noreply.git.farh.net> Date: Wed, 27 May 2026 01:56:53 +0000 Subject: [PATCH 2/7] Remove deploy-dev/deploy-uat CI jobs (CAR-1069) (#37) Co-authored-by: Barcode Betty <32+cs_betty@noreply.git.farh.net> Co-committed-by: Barcode Betty <32+cs_betty@noreply.git.farh.net> --- .gitea/workflows/ci.yml | 87 ----------------------------------------- .mcp.json | 11 ++++++ 2 files changed, 11 insertions(+), 87 deletions(-) create mode 100644 .mcp.json diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 12dcd77..c74a1d1 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -175,90 +175,3 @@ jobs: git tag "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 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ 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 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ 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 \ No newline at end of file diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..6efc1ca --- /dev/null +++ b/.mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "gitea": { + "type": "http", + "url": "https://git-mcp.farh.net/mcp", + "headers": { + "Authorization": "Bearer ${GITEA_TOKEN}" + } + } + } +} -- 2.52.0 From ebf69976d467102497b95588b0f82d0b57fb96c5 Mon Sep 17 00:00:00 2001 From: Barcode Betty <32+cs_betty@noreply.git.farh.net> Date: Mon, 1 Jun 2026 12:38:21 +0000 Subject: [PATCH 3/7] Fix SQLite server_default AttributeError and pool_size errors (#35) Fix SQLite server_default AttributeError and pool_size errors Co-authored-by: Barcode Betty <32+cs_betty@noreply.git.farh.net> Co-committed-by: Barcode Betty <32+cs_betty@noreply.git.farh.net> --- src/cartsnitch_api/database.py | 23 +++++++++++++++-------- tests/conftest.py | 8 +++++++- tests/test_encrypted_json.py | 3 +++ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/cartsnitch_api/database.py b/src/cartsnitch_api/database.py index 3c6043c..1168f4b 100644 --- a/src/cartsnitch_api/database.py +++ b/src/cartsnitch_api/database.py @@ -6,14 +6,21 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_asyn from cartsnitch_api.config import settings -engine = create_async_engine( - settings.database_url, - echo=False, - pool_size=10, - max_overflow=20, - pool_pre_ping=True, - pool_recycle=3600, -) + +def _build_engine_kwargs() -> dict: + url = settings.database_url + kwargs: dict = {"echo": False} + if not url.startswith("sqlite"): + kwargs.update( + pool_size=10, + max_overflow=20, + pool_pre_ping=True, + pool_recycle=3600, + ) + return kwargs + + +engine = create_async_engine(settings.database_url, **_build_engine_kwargs()) async_session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) diff --git a/tests/conftest.py b/tests/conftest.py index 6439552..8bc7d53 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -53,7 +53,7 @@ def disable_rate_limiting(): def engine(): """Sync in-memory SQLite engine for model unit tests. - Strips PostgreSQL-specific server_default expressions so SQLite can + Strips ALL PostgreSQL-specific server_default expressions so SQLite can handle all column inserts without missing-function errors. """ eng = create_engine("sqlite:///:memory:") @@ -62,6 +62,9 @@ def engine(): for col in table.columns.values(): sd = col.server_default if sd is not None: + if not hasattr(sd, "expression"): + col.server_default = None + continue expr_str = str(sd.expression).lower() if "gen_random_uuid" in expr_str or "gen_random_bytes" in expr_str: col.server_default = None @@ -93,6 +96,9 @@ async def db_engine(): for col in table.columns.values(): sd = col.server_default if sd is not None: + if not hasattr(sd, "expression"): + col.server_default = None + continue expr_str = str(sd.expression).lower() if "gen_random_uuid" in expr_str or "gen_random_bytes" in expr_str: col.server_default = None diff --git a/tests/test_encrypted_json.py b/tests/test_encrypted_json.py index 07cf44c..9a38649 100644 --- a/tests/test_encrypted_json.py +++ b/tests/test_encrypted_json.py @@ -22,6 +22,9 @@ def engine(): for col in table.columns.values(): sd = col.server_default if sd is not None: + if not hasattr(sd, "expression"): + col.server_default = None + continue expr_str = str(sd.expression).lower() if "gen_random_uuid" in expr_str or "gen_random_bytes" in expr_str: col.server_default = None -- 2.52.0 From f18df8a40ceb45c49e0f5720852feee7eeaa4c90 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Thu, 28 May 2026 19:25:38 +0000 Subject: [PATCH 4/7] fix: rename loop variable to avoid shadowing SQLAlchemy table import (F402) --- tests/test_encrypted_json.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_encrypted_json.py b/tests/test_encrypted_json.py index 9a38649..08b16d7 100644 --- a/tests/test_encrypted_json.py +++ b/tests/test_encrypted_json.py @@ -18,8 +18,8 @@ from cartsnitch_api.models.user import User, UserStoreAccount def engine(): eng = create_engine("sqlite:///:memory:") - for table in Base.metadata.tables.values(): - for col in table.columns.values(): + for tbl in Base.metadata.tables.values(): + for col in tbl.columns.values(): sd = col.server_default if sd is not None: if not hasattr(sd, "expression"): -- 2.52.0 From bd6b137c68ef0b56eccfb022fddeb24241766505 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Tue, 2 Jun 2026 02:53:46 +0000 Subject: [PATCH 5/7] Fix SQLite timestamp and UUID server_defaults in test fixtures 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 --- tests/conftest.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 8bc7d53..b3a226f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,14 @@ from cartsnitch_api.database import get_db from cartsnitch_api.main import create_app from cartsnitch_api.models import Base + +def _set_timestamp_defaults(mapper, connection, target): + """Populate created_at/updated_at before insert for SQLite compatibility.""" + now = datetime.now(UTC) + for col in [c for c in mapper.columns if c.key in ("created_at", "updated_at")]: + if getattr(target, col.key, None) is None: + setattr(target, col.key, now) + TEST_JWT_SECRET = secrets.token_urlsafe(32) TEST_SERVICE_KEY = secrets.token_urlsafe(32) TEST_FERNET_KEY = "7reF42nmTwbdN21PBoubGp7h_FU8qSimstmlaMLoRK8=" @@ -53,22 +61,28 @@ def disable_rate_limiting(): def engine(): """Sync in-memory SQLite engine for model unit tests. - Strips ALL PostgreSQL-specific server_default expressions so SQLite can - handle all column inserts without missing-function errors. + Strips PostgreSQL-specific server_default expressions and provides + Python-side defaults for SQLite compatibility. """ eng = create_engine("sqlite:///:memory:") - for table in Base.metadata.tables.values(): - for col in table.columns.values(): + for tbl in Base.metadata.tables.values(): + for col in tbl.columns.values(): sd = col.server_default if sd is not None: if not hasattr(sd, "expression"): col.server_default = None continue expr_str = str(sd.expression).lower() - if "gen_random_uuid" in expr_str or "gen_random_bytes" in expr_str: + # Strip PostgreSQL-specific defaults + if any(x in expr_str for x in ["gen_random_uuid", "gen_random_bytes", "now()"]): col.server_default = None + # Register event listener to populate timestamps on insert + for cls in Base.registry._class_registry.values(): + if hasattr(cls, "__mapper__"): + event.listen(cls, "before_insert", _set_timestamp_defaults) + Base.metadata.create_all(eng) yield eng eng.dispose() @@ -92,17 +106,23 @@ async def db_engine(): cursor.execute("PRAGMA foreign_keys=ON") cursor.close() - for table in Base.metadata.tables.values(): - for col in table.columns.values(): + for tbl in Base.metadata.tables.values(): + for col in tbl.columns.values(): sd = col.server_default if sd is not None: if not hasattr(sd, "expression"): col.server_default = None continue expr_str = str(sd.expression).lower() - if "gen_random_uuid" in expr_str or "gen_random_bytes" in expr_str: + # Strip PostgreSQL-specific defaults + if any(x in expr_str for x in ["gen_random_uuid", "gen_random_bytes", "now()"]): col.server_default = None + # Register event listener to populate timestamps on insert + for cls in Base.registry._class_registry.values(): + if hasattr(cls, "__mapper__"): + event.listen(cls, "before_insert", _set_timestamp_defaults) + async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) await conn.execute( -- 2.52.0 From 2b20946ad7ba1796cdda6bb0acd4bb19c3777ec5 Mon Sep 17 00:00:00 2001 From: Barcode Betty Date: Tue, 2 Jun 2026 14:53:16 +0000 Subject: [PATCH 6/7] fix: /health returns 503 on DB failure, pool_timeout=30, CI typecheck fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QA review of PR #39 (CAR-1121) identified three blocking issues; this commit addresses all three plus the typecheck errors flagged as CI RED. CAR-1077 (PR #39) changes: - database.py: add pool_timeout=30 so the engine fails fast when the connection pool is exhausted (defends against the "server closed connection unexpectedly" pod failures). - routes/health.py: /health now calls SELECT 1 through Depends(get_db) and raises HTTPException(503) when the database is unreachable, so Kubernetes readiness probes can correctly mark the pod unhealthy and stop routing traffic to it. Logs the failure at exception level for observability. - Drop .mcp.json from this PR (root-level MCP server config, not related to the pool fix; tracked separately). CI typecheck fixes (pre-existing on dev, were failing mypy on PR #39): - auth/passwords.py: cast bcrypt return values so mypy doesn't widen to Any. - config.py: silence the false-positive call-arg on Settings() — the three required fields are populated from the environment by pydantic-settings at runtime. - cache.py: coerce the bytes/str union returned by the redis client to the documented str | None return type. - middleware/rate_limit.py: annotate the three module-level limiters with the RateLimitBackend protocol, cast the redis zrange score to float before arithmetic, and add max_requests/window_seconds to the protocol so the response-header builder can read them. Co-Authored-By: Paperclip --- .mcp.json | 11 -------- src/cartsnitch_api/auth/passwords.py | 4 +-- src/cartsnitch_api/cache.py | 7 ++++- src/cartsnitch_api/config.py | 2 +- src/cartsnitch_api/database.py | 1 + src/cartsnitch_api/middleware/rate_limit.py | 10 ++++++- src/cartsnitch_api/routes/health.py | 30 ++++++++++++++++++--- 7 files changed, 46 insertions(+), 19 deletions(-) delete mode 100644 .mcp.json diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index 6efc1ca..0000000 --- a/.mcp.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "mcpServers": { - "gitea": { - "type": "http", - "url": "https://git-mcp.farh.net/mcp", - "headers": { - "Authorization": "Bearer ${GITEA_TOKEN}" - } - } - } -} diff --git a/src/cartsnitch_api/auth/passwords.py b/src/cartsnitch_api/auth/passwords.py index 180f994..1205107 100644 --- a/src/cartsnitch_api/auth/passwords.py +++ b/src/cartsnitch_api/auth/passwords.py @@ -4,8 +4,8 @@ import bcrypt def hash_password(password: str) -> str: - return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() + return str(bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()) def verify_password(plain_password: str, hashed_password: str) -> bool: - return bcrypt.checkpw(plain_password.encode(), hashed_password.encode()) + return bool(bcrypt.checkpw(plain_password.encode(), hashed_password.encode())) diff --git a/src/cartsnitch_api/cache.py b/src/cartsnitch_api/cache.py index 319cb8d..6766a8c 100644 --- a/src/cartsnitch_api/cache.py +++ b/src/cartsnitch_api/cache.py @@ -35,7 +35,12 @@ class CacheClient: async def get(self, key: str) -> str | None: if not self._client: return None - return await self._client.get(key) + value = await self._client.get(key) + if value is None: + return None + if isinstance(value, bytes): + return value.decode("utf-8", errors="replace") + return value async def set(self, key: str, value: str, ttl_seconds: int = 300) -> None: if not self._client: diff --git a/src/cartsnitch_api/config.py b/src/cartsnitch_api/config.py index c71d753..b82aa37 100644 --- a/src/cartsnitch_api/config.py +++ b/src/cartsnitch_api/config.py @@ -86,4 +86,4 @@ class Settings(BaseSettings): return self -settings = Settings() +settings = Settings() # type: ignore[call-arg] diff --git a/src/cartsnitch_api/database.py b/src/cartsnitch_api/database.py index 1168f4b..5334b84 100644 --- a/src/cartsnitch_api/database.py +++ b/src/cartsnitch_api/database.py @@ -14,6 +14,7 @@ def _build_engine_kwargs() -> dict: kwargs.update( pool_size=10, max_overflow=20, + pool_timeout=30, pool_pre_ping=True, pool_recycle=3600, ) diff --git a/src/cartsnitch_api/middleware/rate_limit.py b/src/cartsnitch_api/middleware/rate_limit.py index af3dd4b..b32f760 100644 --- a/src/cartsnitch_api/middleware/rate_limit.py +++ b/src/cartsnitch_api/middleware/rate_limit.py @@ -25,6 +25,9 @@ logger = logging.getLogger(__name__) class RateLimitBackend(Protocol): """Protocol for rate limit backends.""" + max_requests: int + window_seconds: int + async def is_allowed(self, key: str) -> tuple[bool, int, int]: """Check if request is allowed. Returns (allowed, remaining, retry_after).""" @@ -82,7 +85,8 @@ class RedisSlidingWindow: if current_count >= self.max_requests: oldest = await self.redis.zrange(key, 0, 0, withscores=True) if oldest: - retry_after = int((oldest[0][1] - cutoff) / 1000) + 1 + oldest_score = float(oldest[0][1]) + retry_after = int((oldest_score - cutoff) / 1000) + 1 else: retry_after = self.window_seconds return False, 0, retry_after @@ -114,6 +118,10 @@ if settings.rate_limit_redis_enabled: logger.warning("Failed to connect to Redis for rate limiting, using in-memory: %s", e) _use_redis = False +_public_limiter: RateLimitBackend +_auth_limiter: RateLimitBackend +_auth_strict_limiter: RateLimitBackend + if _use_redis and _redis_client: _public_limiter = RedisSlidingWindow( _redis_client, settings.rate_limit_requests, settings.rate_limit_window_seconds diff --git a/src/cartsnitch_api/routes/health.py b/src/cartsnitch_api/routes/health.py index 0574b10..dce47f2 100644 --- a/src/cartsnitch_api/routes/health.py +++ b/src/cartsnitch_api/routes/health.py @@ -1,16 +1,40 @@ """Health check and error metrics endpoints.""" -from fastapi import APIRouter, Depends +import logging + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import text +from sqlalchemy.ext.asyncio import AsyncSession from cartsnitch_api.auth.dependencies import verify_service_key +from cartsnitch_api.database import get_db from cartsnitch_api.middleware.error_handler import get_error_monitor +logger = logging.getLogger(__name__) + router = APIRouter(tags=["health"]) @router.get("/health") -async def health(): - return {"status": "ok"} +async def health(db: AsyncSession = Depends(get_db)): + """Liveness + DB connectivity probe. + + Returns HTTP 200 when the API process is responsive *and* the database + is reachable, so Kubernetes readiness probes can correctly route traffic + away from pods that have lost their database connection. + + Returns HTTP 503 when the database is unreachable so K8s marks the pod + unhealthy and stops sending traffic to it. + """ + try: + await db.execute(text("SELECT 1")) + except Exception as exc: + logger.exception("Health check failed: database unreachable") + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail={"status": "unavailable", "database": "disconnected"}, + ) from exc + return {"status": "ok", "database": "connected"} @router.get("/internal/error-stats", dependencies=[Depends(verify_service_key)]) -- 2.52.0 From 76781ed2385764a690b8835fb4750b101ddff52a Mon Sep 17 00:00:00 2001 From: Barcode Betty Date: Tue, 2 Jun 2026 14:58:18 +0000 Subject: [PATCH 7/7] style: fix ruff format in conftest.py 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 --- tests/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/conftest.py b/tests/conftest.py index b3a226f..c9dc552 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,7 @@ def _set_timestamp_defaults(mapper, connection, target): if getattr(target, col.key, None) is None: setattr(target, col.key, now) + TEST_JWT_SECRET = secrets.token_urlsafe(32) TEST_SERVICE_KEY = secrets.token_urlsafe(32) TEST_FERNET_KEY = "7reF42nmTwbdN21PBoubGp7h_FU8qSimstmlaMLoRK8=" -- 2.52.0