import os import sys # Must be set before any repositories module is imported — each module # selects its Mock* implementation vs the real PocketBase-backed one at # import time based on this flag. os.environ['USE_MOCK_REPOSITORIES'] = 'true' os.environ.setdefault('API_SECRET_KEY', 'test-api-key') os.environ.setdefault('POCKETBASE_URL', 'http://pocketbase.invalid') os.environ.setdefault('REDIS_URL', 'redis://localhost:6379/0') # Keeps Flask-Limiter off the network entirely in tests — see extensions.py. os.environ.setdefault('RATELIMIT_STORAGE_URI', 'memory://') os.environ.setdefault('FIRECRAWL_URL', 'http://firecrawl.invalid') os.environ.setdefault('GOTIFY_URL', 'http://gotify.invalid') os.environ.setdefault('GOTIFY_APP_TOKEN', 'test-token') # Force-cleared (not setdefault) rather than left alone: app.py's # load_dotenv() runs on every `client`/`app` fixture use and picks up # whatever real credentials happen to be sitting in backend/.env on # the machine running the suite — nothing above protects these, since # they were never set here before. With real values, SENTRY_DSN # actually initialises Sentry and ships real events during test runs, # and STRIPE_PRICE_DISCOVER stops being "unset" for the one test that # depends on it being absent. Setting them to '' here wins that race — # load_dotenv() never overrides an already-present env var, even an # empty one — so tests stay hermetic regardless of what's in .env. for _leaky_var in ( 'SENTRY_DSN', 'STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET', 'STRIPE_PRICE_ANALYSE', 'STRIPE_PRICE_DISCOVER', 'STRIPE_PRICE_INTELLIGENCE', 'RESEND_API_KEY', 'OPENROUTER_API_KEY', 'WEBSHARE_USER', 'WEBSHARE_PASS', 'BRIGHTDATA_USER', 'BRIGHTDATA_PASS', 'HETZNER_S3_ACCESS_KEY', 'HETZNER_S3_SECRET_KEY', 'GOOGLE_SERVICE_ACCOUNT_JSON', 'SHEET_ID', ): os.environ[_leaky_var] = '' sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) import pytest @pytest.fixture def app(): from app import app as flask_app flask_app.config['TESTING'] = True return flask_app @pytest.fixture def client(app): return app.test_client() @pytest.fixture def api_headers(): return {'X-API-Key': 'test-api-key'} @pytest.fixture(autouse=True) def _reset_rate_limiter(): """ Flask-Limiter's memory:// storage is a module-level singleton (extensions.py's `limiter` is created once at import time) — its hit counters persist across the whole pytest session, not per test. Without this, any route with a tight limit (e.g. "3 per day") starts failing tests with 429s once enough tests across the whole suite happen to call it, regardless of which test "used up" the quota. """ yield from extensions import limiter # Only initialised once some test in this session has actually used # the `app`/`client` fixture (app.py's limiter.init_app(app)) — pure # service-layer tests never touch Flask at all, and limiter.reset() # asserts on a storage backend that was never attached. if getattr(limiter, '_storage', None) is not None: limiter.reset() @pytest.fixture(autouse=True) def _reset_mock_repositories(): """ Clears every mock repository's in-memory store between tests so state doesn't leak across test functions. """ yield from repositories.tenant_repository import tenant_repository from repositories.seat_repository import seat_repository from repositories.competitor_repository import competitor_repository from repositories.product_repository import product_repository from repositories.scrape_repository import scrape_repository from repositories.proxy_repository import proxy_repository from repositories.alert_repository import alert_repository from repositories.battlecard_repository import battlecard_repository from repositories.comparable_repository import comparable_repository from repositories.monitoring_repository import monitoring_repository from repositories.price_history_repository import price_history_repository from repositories.client_trips_repository import client_trips_repository from repositories.ngs_meta_repository import ngs_meta_repository from repositories.watchlist_repository import watchlist_repository for repo in [ tenant_repository, seat_repository, competitor_repository, product_repository, scrape_repository, proxy_repository, alert_repository, battlecard_repository, comparable_repository, monitoring_repository, price_history_repository, client_trips_repository, ngs_meta_repository, watchlist_repository, ]: if hasattr(repo, '_records'): repo._records.clear()