bober-bbq-bot/docs/TESTING.md
byrsapty 375022e739 Add backup_remote.py and wire up the rest of the rclone rework
Continuation of the previous commit (gdrive_backup.py removal landed
separately by accident) — adds the new module itself, the admin
routes/template using it, generalized Setting keys, updated docs/
provisioning notes, and tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 22:57:59 +03:00

75 lines
5.3 KiB
Markdown

# Automated tests
A repo-committed `pytest` suite lives under `tests/`. It replaces the
ad-hoc "write a one-off script to a scratch temp dir, run it by hand"
verification method used throughout this project's development — every
test here follows the same pattern those scripts used (a fresh temp
SQLite DB, `create_app()`, `db.create_all()` + `migrate()` + `seed()`,
monkeypatching a module's `send_message` / `fiscalize_in_background` /
`get_invoice_status` attribute directly instead of pulling in a mocking
framework), just organized into real fixtures and committed so it runs in
CI / before every deploy instead of living only in someone's memory.
## Running it
```bash
pip install -r requirements-dev.txt
pytest
```
`pytest.ini` at the repo root points `pytest` at `tests/` and needs no
extra flags — just run it from the repo root (or the worktree root).
Each test gets its own fresh temp SQLite database file (created and torn
down per test in `tests/conftest.py`'s `app` fixture) — slower than a
single shared DB, but immune to one test's leftover state (an order, a
lockout, a supplier) leaking into another. Module-level shared state used
intentionally in production for cross-request coordination (login
lockouts, the error-alert cooldown, the Checkbox token cache) is also
explicitly reset between tests — see `_reset_module_level_state()` in
`conftest.py`.
## What's covered
| Module | Test file | What's tested |
| --- | --- | --- |
| `bober_bbq/payments/monobank.py` | `tests/test_payments.py` | `claim_order_paid()` atomicity (first caller wins, second loses); the Monobank webhook, checkout poll, and reconciliation job never double-fire paid-order side effects for the same invoice |
| `bober_bbq/utils/checkbox_prro.py` | `tests/test_fiscalization_alerts.py` | `_mark_error()` alerts every owner, records `fiscal_status`/`fiscal_error`, and respects `error_alerts`' per-(exception, source) cooldown |
| `bober_bbq/utils/login_throttle.py` + `/admin/login` | `tests/test_login_throttle.py` | Lockout after `MAX_ATTEMPTS` wrong passwords from one IP (even the correct password is then rejected), an unaffected sibling IP, lockout expiry, and clearing the failure counter on success |
| `run_web._warn_on_insecure_defaults` | `tests/test_startup_checks.py` | Alerts the owner when `SECRET_KEY`/`FLASK_ADMIN_PASSWORD` are still the hardcoded defaults; stays silent once overridden |
| `bober_bbq/utils/supplier_import.py` | `tests/test_supplier_import_security.py` | `_resolve_upload_path()` rejects absolute paths (both slash styles), `../`/`..\` traversal, a wrong-shape-but-real-extension token, and an empty string — all with `FileNotFoundError`, none touching a file outside the upload temp dir; a legitimate token still round-trips |
| `bober_bbq/admin/suppliers.py` | `tests/test_inventory_suppliers.py` | `_apply_stock_topup()` credits stock, auto-assigns `Ingredient.supplier_id` only when unset (never overwrites an existing link), and leaves `StockMovement.note` exactly as typed (regression test for a real bug where it was getting overwritten with the supplier's name) |
| `bober_bbq/api/checkout.py` | `tests/test_checkout.py` | Delivery and pickup order creation via the checkout API produces an `Order` row with sane fields (type, totals, address/pickup fields, linked items) and clears the cart; an empty cart is rejected |
## What's explicitly NOT covered yet
- **The React webapp** (`webapp/`) has no automated tests at all (no unit
tests, no component tests, no E2E). It's tested manually in the browser.
- **The Telegram bot's conversation flows** (`bot/`) — aiogram handlers,
FSM states, inline keyboards — aren't covered beyond what the checkout
HTTP API itself exercises (`bot/services/order_service.py` and
`bot/services/cart_service.py`, indirectly, via `tests/test_checkout.py`).
A real bot conversation test would need an aiogram test harness/fake
`Bot` transport, which doesn't exist yet.
- **Checkbox (ПРРО) fiscalization's happy path** — `create_receipt()`
actually calling out to Checkbox's API (signin, shift open/close,
receipt creation, PDF polling) isn't exercised; only the error-handling
side (`_mark_error` / alerting) is. It's all `requests` calls to a real
third-party API with no local fake/sandbox available.
- **Monobank invoice creation** (`create_invoice`, `verify_token`,
webhook-signature verification against a real Monobank public key) —
only the *webhook handler's* payment-status logic is tested, with
signature verification monkeypatched out. Testing the actual HTTP calls
would need a fake Monobank server.
- **Remote backups' actual cloud calls** (`bober_bbq/utils/backup_remote.py`)
`tests/test_backup_remote.py` covers the env/path construction per
storage type and the rotation logic with `subprocess.run` mocked, but
never shells out to a real `rclone` binary or a real cloud provider.
- **The scheduled jobs' timing/APScheduler wiring itself** in `run_web.py`
(only the job *functions* they call are tested directly, e.g.
`reconcile_pending_payments()`).
- **Admin panel pages beyond `/admin/login`** — the rest of the Jinja
admin UI (order management, menu editing, reports, settings screens) has
no route-level tests here; `tests/test_inventory_suppliers.py` calls
`_apply_stock_topup()` directly rather than through its HTTP route for
simplicity.