Files
shopdb-flask/tests/conftest.py
cproudlock 2d1bb83c3b Phase 1: pytest baseline, production hardening, pinned requirements
Establishes the safety net required before any structural refactor.

Tests (tests/):
- conftest.py rewritten for Flask-SQLAlchemy 3.x (drop-recreate per
  test, StaticPool-shared in-memory SQLite, admin_user + auth_headers
  fixtures). Removes deprecated db.create_scoped_session pattern.
- test_smoke.py: 8 baseline tests (app boot, JWT login valid+invalid,
  protected routes, paginated response shape, plugin auto-discovery).
- test_security_config.py: 7 tests pinning ProductionConfig.validate
  failure modes (missing/dev SECRET_KEY, missing JWT_SECRET_KEY,
  missing DATABASE_URL, wildcard CORS, empty CORS) and one happy-path.

Production hardening (shopdb/config.py, shopdb/__init__.py):
- ProductionConfig.validate() raises ConfigError on missing or
  insecure SECRET_KEY, JWT_SECRET_KEY, DATABASE_URL, CORS_ORIGINS.
  No silent fallback to dev defaults in production.
- create_app invokes validate() when config_name == 'production'.
- CORS_ORIGINS default no longer wildcard; defaults to localhost
  Vite dev origin.
- Drop os.path.exists probe in serve_frontend (path-traversal risk
  surface). send_from_directory handles safe-join + 404 itself.
- Replace User.query.get with db.session.get (SQLAlchemy 2.0 API).

TestingConfig (shopdb/config.py):
- Add StaticPool + check_same_thread connect_args so SQLite in-memory
  is shared across the test session.

Index dedup (plugins/printers/models/printer_extension.py):
- Rename idx_printer_windowsname -> idx_printerdata_windowsname.
  Two model classes (Printer, PrinterData) declared the same index
  name; SQLite enforces global index uniqueness even across tables.
  Per CONTRIBUTING.md naming convention, indexes follow
  idx_<table>_<column>.

Dependency pinning (requirements.in, requirements.txt):
- requirements.in holds the loose source pins (the human-edited file).
- requirements.txt is now a uv-compiled lockfile (every transitive
  dep pinned to an exact version). Reproducible builds. Run
  `uv pip compile requirements.in -o requirements.txt` to refresh.

Test count: 0 -> 15 passing. All naming/style checks still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 14:48:19 -04:00

85 lines
2.2 KiB
Python

"""Pytest configuration and fixtures for shopdb-flask.
Strategy: in-memory SQLite via StaticPool (configured in TestingConfig)
so the database is shared across the connection. Each test drops and
recreates the schema. Simple, totally isolated, fast enough for a small
schema. Switch to savepoint-per-test if test count grows past a few
hundred.
"""
import os
import pytest
from werkzeug.security import generate_password_hash
# Force testing config before any shopdb import touches the env.
os.environ['FLASK_ENV'] = 'testing'
from shopdb import create_app
from shopdb.extensions import db as _db
@pytest.fixture(scope='session')
def app():
"""Create the Flask application for the test session."""
application = create_app('testing')
return application
@pytest.fixture(scope='function')
def db(app):
"""Provide a fresh database per test. Drops and recreates schema each run."""
with app.app_context():
_db.create_all()
yield _db
_db.session.remove()
_db.drop_all()
@pytest.fixture
def client(app):
"""Flask test client."""
return app.test_client()
@pytest.fixture
def runner(app):
"""Flask CLI test runner."""
return app.test_cli_runner()
@pytest.fixture
def admin_user(db):
"""Create an admin user for authenticated tests.
The user has username 'testadmin' and password 'testpass'.
"""
from shopdb.core.models import User, Role
role = Role(rolename='admin', description='Administrator')
db.session.add(role)
db.session.flush()
user = User(
username='testadmin',
email='admin@test.local',
passwordhash=generate_password_hash('testpass'),
)
user.roles.append(role)
db.session.add(user)
db.session.commit()
return user
@pytest.fixture
def auth_headers(client, admin_user):
"""Log in as admin_user and return Authorization headers."""
response = client.post(
'/api/auth/login',
json={'username': 'testadmin', 'password': 'testpass'},
)
assert response.status_code == 200, f'Login failed: {response.get_json()}'
payload = response.get_json()
token = payload['data']['access_token']
return {'Authorization': f'Bearer {token}'}