app.bettersight.io/backend/tests/conftest.py

108 lines
4.5 KiB
Python

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
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,
]:
if hasattr(repo, '_records'):
repo._records.clear()