The equipment plugin is now the machines plugin, ending the UI-vs-code vocabulary split while the contract is pre-1.0 and nothing external depends on the old names. - plugins/equipment -> plugins/machines: manifest, class, /api/machines, machines.* permissions, registry key (with an auto-migrating load shim for existing installs). - Tables: equipment -> machines (equipmentid -> machineid) and equipmenttypes -> machinetypes, renamed in the plugin's own migration chain (machines0002rename), idempotent for both upgrading and fresh installs. - The legacy core machinetypes lookup actually types the vendor MODELS catalog, so it is renamed losslessly to modeltypes (models.modeltypeid, /api/modeltypes, Model Types settings page) rather than collapsed, freeing the machinetypes name. Core migration 7d17_machines_rename also flips data in place: assettypes row equipment -> machine, auditlog entitytype, identifier_/search_ settings keys, permission rows, and renames alembic_version_equipment. - Frontend: machinesApi/modeltypesApi, item.machine response shape, assettype value compares 'equipment' -> 'machine' (map, search, custom fields, relationships), routes machines.js with plugin gating retagged, /print/machine-badge, Machine Types (subtypes) and Model Types (catalog) settings pages, machines-by-type report id. - Docs swept; ADRs left as history per the authoring rule. Upgrade: flask db upgrade then flask plugin upgrade-all. Verified: dev DB flipped live (262 machines, 35 modeltypes, 95 models retyped, zero equipment tables remain); fresh scratch-MySQL install produces the new names; 341 tests green; naming/style green; frontend builds; live E2E on machines list/detail, PC relationships, map, reports, and both settings pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
114 lines
3.8 KiB
Python
114 lines
3.8 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',
|
|
'employees',
|
|
'knowledgebase',
|
|
'machines',
|
|
'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}'
|
|
)
|