bober-bbq-bot/tests/conftest.py
byrsapty 8bba2873b5 Add repo-committed pytest test suite
Converts the ad-hoc scratch-script verification pattern used throughout
this session's development (temp SQLite DB + create_app() + monkeypatched
send_message/fiscalize_in_background/get_invoice_status, print-based
check() assertions) into a real, repo-committed pytest suite under tests/.

Covers: claim_order_paid() atomicity, cross-path payment race dedup
(webhook/checkout-poll/reconciliation all observing the same paid
invoice), fiscalization error alerting + cooldown, admin login
brute-force lockout, the insecure-defaults startup check, supplier-import
path-traversal protection, basic checkout smoke, and the supplier
restock -> stock/supplier-link bug fixed earlier this session.

Adds requirements-dev.txt (pytest only, keeping the existing
monkeypatch-only style rather than a new mocking framework), pytest.ini
so `pytest` runs from the repo root with no flags, and docs/TESTING.md
covering how to run it and what's explicitly not covered yet (the React
webapp, bot conversation flows beyond the checkout API, live
Checkbox/Monobank/Google Drive integrations).

38 tests, all passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 13:32:09 +03:00

116 lines
4.6 KiB
Python

"""Shared pytest fixtures for the Bober BBQ test suite.
Converts the ad-hoc "scratch smoke script" pattern used throughout this
project's development (a temp SQLite file per run, `create_app()`,
`db.create_all()` + `migrate()` + `seed()`, monkeypatching modules'
`send_message` / `fiscalize_in_background` / `get_invoice_status`
attributes directly instead of a mocking framework) into a proper,
repo-committed pytest fixture.
Each test gets its OWN fresh temp SQLite database (safer against state
leakage between tests than a session-scoped DB, at the cost of a little
extra setup time — `db.create_all()` + `seed()` against SQLite is fast
enough that this doesn't matter in practice).
"""
import os
import sys
import tempfile
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parent.parent
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
# Must be set before the FIRST import of bober_bbq.config anywhere in the
# process, since Config's class attributes are evaluated once at import
# time. conftest.py is imported by pytest before any test module gets
# collected, so this is early enough.
os.environ.setdefault("SECRET_KEY", "test-secret")
os.environ.setdefault("BOT_TOKEN", "")
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
# NOT "admin" on purpose: run_web.py's module-level `_warn_on_insecure_
# defaults()` call (see below) must find nothing wrong the moment run_web
# is first imported, or it consumes error_alerts' one-per-cooldown-window
# alert slot before test_startup_checks.py ever gets to exercise it.
os.environ.setdefault("FLASK_ADMIN_PASSWORD", "a-real-non-default-test-password")
from bober_bbq.app import create_app # noqa: E402
from bober_bbq.config import config # noqa: E402
from bober_bbq.extensions import db # noqa: E402
from bober_bbq.migrate import migrate # noqa: E402
from bober_bbq.seed import seed # noqa: E402
from bober_bbq.utils import checkbox_prro, error_alerts, login_throttle, reconciliation # noqa: E402
# Imported here, once, with the safe SECRET_KEY/FLASK_ADMIN_PASSWORD env
# defaults already in place above — run_web.py runs `_warn_on_insecure_
# defaults()` unconditionally at module import time (not just under
# `if __name__ == "__main__"`), so importing it for the first time with
# insecure values already in place would fire (and consume the cooldown
# for) the very alert test_startup_checks.py exists to test. Tests call
# `run_web._warn_on_insecure_defaults()` directly afterward against
# whatever config values they set up.
import run_web # noqa: E402
def _reset_module_level_state():
"""These modules intentionally use module-level dicts as shared state
across request threads in production (see their own docstrings/
comments) — e.g. login_throttle's lockout tracking, error_alerts'
per-(exception, source) cooldown. That state must NOT leak between
tests, or an early test's alert cooldown / lockout would silently
suppress a later, unrelated test's assertions."""
login_throttle._failures.clear()
login_throttle._locked_until.clear()
error_alerts._last_sent.clear()
checkbox_prro._token_cache.update({"token": None, "at": 0.0})
reconciliation._last_token_ok = None
@pytest.fixture
def app():
"""A fresh Flask app wired to its own temp SQLite DB, seeded with the
normal default data (categories/products/settings/admin user)."""
tmp_db = tempfile.NamedTemporaryFile(suffix=".db", delete=False)
tmp_db.close()
# config is a module-level singleton (bober_bbq.config.config); override
# the instance attribute directly rather than the env var, since the
# Config class attribute was already evaluated at import time.
config.SQLALCHEMY_DATABASE_URI = f"sqlite:///{tmp_db.name}"
config.OWNER_IDS = []
flask_app = create_app()
flask_app.config["WTF_CSRF_ENABLED"] = False
flask_app.testing = True
# Enables the ?dev_user_id=... Telegram-auth bypass in
# api/auth.require_telegram_auth (see its own comment) — there is no
# real Telegram WebView in tests to sign init data with.
flask_app.debug = True
with flask_app.app_context():
db.create_all()
migrate()
seed()
_reset_module_level_state()
yield flask_app
with flask_app.app_context():
db.session.remove()
db.engine.dispose()
try:
os.unlink(tmp_db.name)
except PermissionError:
# Windows sometimes still holds the file handle briefly after
# dispose() — matches the original scratch-script behavior of
# tolerating this rather than failing the whole run over cleanup.
pass
@pytest.fixture
def client(app):
return app.test_client()