Second core feature pulled into a plugin. Slides is the minimal plugin shape: a blueprint only - no model, no AssetType, no nav (it is purely an API surface consumed by the TV dashboard). - plugins/slides/: manifest (api_prefix /api/slides, no deps), api/ (moved blueprint, contract-pure imports via shopdb.api, same /api/slides route so the TV dashboard is unchanged), plugin.py (get_blueprint, get_models -> []). - Fixed a latent bug in the move: error path used ErrorCodes.SERVER_ERROR which does not exist -> ErrorCodes.INTERNAL_ERROR. - De-cored: deleted shopdb/core/api/slides.py, removed from CORE_BLUEPRINT_NAMES + core/api/__init__ import/__all__. Registered in instance/plugins.json. Pinned with a characterization test first; passes unchanged against the plugin blueprint. 174 tests pass, naming green, app boots 8 bundled plugins, /api/slides verified live. Note: the shopfloor dashboard is NOT extractable - it is a frontend-only Vue view aggregating the businessunits + notifications APIs, with no backend to move and no frontend plugin system. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
113 lines
3.7 KiB
Python
113 lines
3.7 KiB
Python
"""Smoke tests pinning the baseline behavior of shopdb-flask.
|
|
|
|
These eight tests are the safety net required before any structural
|
|
refactor proceeds. See `~/.claude/skills/pinning-flask-behavior.md`.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
|
|
def test_app_factory_creates_app(app):
|
|
"""create_app('testing') returns a Flask app with TESTING=True."""
|
|
assert app is not None
|
|
assert app.config['TESTING'] is True
|
|
assert 'sqlite' in app.config['SQLALCHEMY_DATABASE_URI']
|
|
|
|
|
|
def test_login_with_valid_credentials_returns_tokens(client, admin_user):
|
|
"""POST /api/auth/login with valid creds returns access and refresh tokens."""
|
|
response = client.post(
|
|
'/api/auth/login',
|
|
json={'username': 'testadmin', 'password': 'testpass'},
|
|
)
|
|
assert response.status_code == 200
|
|
payload = response.get_json()
|
|
assert 'data' in payload
|
|
data = payload['data']
|
|
assert 'access_token' in data
|
|
assert 'refresh_token' in data
|
|
assert 'user' in data
|
|
assert data['user']['username'] == 'testadmin'
|
|
|
|
|
|
def test_login_with_invalid_credentials_returns_401(client, admin_user):
|
|
"""Wrong password returns 401 with the documented error envelope.
|
|
|
|
Pins the current shape: error info nested under `data.error` (not at
|
|
top level). The error_response docstring claims top-level `error` but
|
|
the implementation puts it under `data`. Pinned as-is until that
|
|
inconsistency is intentionally addressed.
|
|
"""
|
|
response = client.post(
|
|
'/api/auth/login',
|
|
json={'username': 'testadmin', 'password': 'wrongpassword'},
|
|
)
|
|
assert response.status_code == 401
|
|
payload = response.get_json()
|
|
assert payload['status'] == 'error'
|
|
assert payload['data']['error']['code'] == 'UNAUTHORIZED'
|
|
|
|
|
|
def test_login_with_missing_fields_returns_400(client):
|
|
"""Missing username or password returns 400 validation error."""
|
|
response = client.post('/api/auth/login', json={})
|
|
assert response.status_code == 400
|
|
|
|
|
|
def test_protected_route_requires_authentication(client, admin_user):
|
|
"""GET /api/users without a JWT returns 401."""
|
|
response = client.get('/api/users')
|
|
assert response.status_code == 401
|
|
|
|
|
|
def test_protected_route_works_with_jwt(client, auth_headers):
|
|
"""GET /api/users with a valid JWT returns 200."""
|
|
response = client.get('/api/users', headers=auth_headers)
|
|
assert response.status_code == 200
|
|
|
|
|
|
def test_paginated_response_shape(client, auth_headers):
|
|
"""A paginated list endpoint returns data plus pagination meta.
|
|
|
|
Uses /api/locations because it is a simple platform endpoint that
|
|
uses paginated_response. Pagination meta keys follow the naming
|
|
convention (lowercase concatenated): page, perpage, total,
|
|
totalpages, hasnext, hasprev.
|
|
"""
|
|
response = client.get('/api/locations', headers=auth_headers)
|
|
assert response.status_code == 200
|
|
payload = response.get_json()
|
|
assert 'data' in payload
|
|
assert isinstance(payload['data'], list)
|
|
assert 'meta' in payload
|
|
assert 'pagination' in payload['meta']
|
|
pagination = payload['meta']['pagination']
|
|
assert 'page' in pagination
|
|
assert 'perpage' in pagination
|
|
assert 'total' in pagination
|
|
assert 'totalpages' in pagination
|
|
|
|
|
|
def test_plugin_loader_discovers_bundled_plugins(app):
|
|
"""Plugin manager finds the bundled plugins."""
|
|
from shopdb.plugins import plugin_manager
|
|
|
|
expected_plugins = {
|
|
'computers',
|
|
'equipment',
|
|
'knowledgebase',
|
|
'network',
|
|
'notifications',
|
|
'printers',
|
|
'slides',
|
|
'usb',
|
|
}
|
|
|
|
with app.app_context():
|
|
loader = plugin_manager.loader
|
|
discovered = set(loader.discover_plugins())
|
|
|
|
assert expected_plugins.issubset(discovered), (
|
|
f'Missing bundled plugins: {expected_plugins - discovered}'
|
|
)
|