Initial import: shopdb-flask multi-site asset platform
Flask + Vue rewrite of the classic-ASP shopdb, built as a plugin framework so sister GE Aerospace sites can adopt it. Product version 0.7.0-pre-0.8.0, plugin contract 0.11.0, 966 backend tests. Key entry points: - docs/INSTALL-WINDOWS-IIS.md - tested Windows + IIS install runbook (own-site and subpath deployment methods) - docs/PILOT-DEPLOY.md - production pilot: stand up + import + verify - docs/IMPORT-API.md - schema-agnostic legacy data import contract - scripts/site_imports/wjf/ - West Jefferson reference import loader - docs/adr/ - twelve accepted architecture decision records
This commit is contained in:
148
tests/conftest.py
Normal file
148
tests/conftest.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""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}'}
|
||||
Reference in New Issue
Block a user