"""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 json import os from datetime import datetime, timezone from pathlib import Path 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 def _bootstrap_plugin_registry(): # Fresh clones (CI) have no instance/plugins.json - the registry is # gitignored dev state - so no plugins would load and every plugin-backed # test fails. Seed a registry enabling all bundled plugins. An existing # dev registry is left untouched. repo = Path(__file__).resolve().parent.parent state_file = repo / 'instance' / 'plugins.json' if state_file.exists(): return stamp = datetime.now(timezone.utc).replace(tzinfo=None).isoformat() plugins = {} for manifest_path in sorted((repo / 'plugins').glob('*/manifest.json')): manifest = json.loads(manifest_path.read_text()) plugins[manifest['name']] = { 'name': manifest['name'], 'version': manifest.get('version', '1.0.0'), 'installed_at': stamp, 'enabled': True, 'migrations_applied': [], 'config': {}, } state_file.parent.mkdir(parents=True, exist_ok=True) state_file.write_text(json.dumps({'plugins': plugins}, indent=2)) _bootstrap_plugin_registry() @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}'} @pytest.fixture def member_user(db): """Create an authenticated user with NO roles/permissions. Username 'testmember', password 'testpass'. Used to prove that authentication alone does not grant write access (authorization gating). """ from shopdb.core.models import User user = User( username='testmember', email='member@test.local', passwordhash=generate_password_hash('testpass'), ) db.session.add(user) db.session.commit() return user @pytest.fixture def member_headers(client, member_user): """Log in as member_user (no roles) and return Authorization headers.""" response = client.post( '/api/auth/login', json={'username': 'testmember', 'password': 'testpass'}, ) assert response.status_code == 200, f'Login failed: {response.get_json()}' token = response.get_json()['data']['access_token'] return {'Authorization': f'Bearer {token}'}