Apply skill-driven review fixes: security, hook isolation, tests, docs
Addresses findings from a 6-lens review against the project skills (defining-asset-contract, enforcing-plugin-contract, hardening-flask-config, integrating-plugin-hooks, pinning-flask-behavior, simplifying-python). Security (hardening-flask-config): - Load per-plugin COLLECTOR_API_KEY_<PLUGIN> from env in create_app. from_object only copies class attributes, so per-plugin keys (ADR-006) were dead in real deploys and silently fell back to the shared key. - EMPLOYEE_DB_USER/PASSWORD no longer default to root/rootpassword (no safe default for a secret; unset fails loud). Documented in .env.example + DEPLOY.md. - COLLECTOR_API_KEY + per-plugin + EMPLOYEE_DB_* added to .env.example/DEPLOY.md. Hook isolation (integrating-plugin-hooks): - collector _collector_plugins and dashboard get_navigation now re-raise in dev/test and log+isolate in prod, instead of silently swallowing a broken plugin hook. Plugin loader (enforcing-plugin-contract): - enable_plugin/install_plugin read dependencies+version from the manifest instead of instantiating the plugin class. - _register_plugin_components rejects a second plugin claiming an already-used api_prefix (reset per app in init_app). Tests (pinning-flask-behavior): - test_identifiers.py: gauge/maintenance round-trip on computer/printer/network create+update; per-type seed yields the 12 identifier keys. - contract tests for apply_collector_payload presence + schema-declarers-implement. - security tests for per-plugin key env loading + no employee-db password default. Docs/contract sync (defining-asset-contract): - PLUGIN-HOOKS.md documents apply_collector_payload; stale 0.2.0 -> 0.3.0. - ADR-006 documents apply_collector_payload + single-dispatch rationale. - ADR-001 enumerates the expanded shopdb.api import surface. Simplify (simplifying-python): - De-duplicate the 21-entry settings defaults: shared build_default_settings() used by both the /settings/seed route and the CLI (were drifting copies). - Remove dead AssetStatus import + redundant AssetType local import in computers plugin; comment the statusid=1 collector default. 153 tests pass (was 145), naming/style green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
77
tests/test_core/test_identifiers.py
Normal file
77
tests/test_core/test_identifiers.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Tests for per-asset-type optional identifiers (gauge/maintenance refs).
|
||||
|
||||
Pins two behaviors that shipped without coverage:
|
||||
1. gaugelabreference + maintenancereference round-trip through each plugin's
|
||||
asset create AND update endpoints (computer, printer, network).
|
||||
2. The per-type identifier seed produces one setting per
|
||||
(identifier x asset type) pair.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from shopdb.core.api.settings import IDENTIFIER_LABELS, IDENTIFIER_ASSETTYPES
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def asset_types(db):
|
||||
"""Seed the asset types the create endpoints look up by name."""
|
||||
from shopdb.core.models import AssetType
|
||||
|
||||
for name in ('computer', 'printer', 'network_device'):
|
||||
db.session.add(AssetType(assettype=name, pluginname=name,
|
||||
tablename=name, description=name))
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# (endpoint prefix, extension key in the response, extra create fields)
|
||||
PLUGIN_CASES = [
|
||||
('/api/computers', 'computer', {}),
|
||||
('/api/printers', 'printer', {}),
|
||||
('/api/network', 'networkdevice', {}),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('prefix,extkey,extra', PLUGIN_CASES)
|
||||
def test_gauge_maintenance_roundtrip(client, db, auth_headers, asset_types,
|
||||
prefix, extkey, extra):
|
||||
"""Gauge/maintenance refs persist on create and update for each type."""
|
||||
payload = {
|
||||
'assetnumber': f'AST-{extkey}',
|
||||
'gaugelabreference': 'GL-1',
|
||||
'maintenancereference': 'MNT-1',
|
||||
**extra,
|
||||
}
|
||||
created = client.post(prefix, json=payload, headers=auth_headers)
|
||||
assert created.status_code == 201, created.get_json()
|
||||
body = created.get_json()['data']
|
||||
assert body['gaugelabreference'] == 'GL-1'
|
||||
assert body['maintenancereference'] == 'MNT-1'
|
||||
|
||||
extid = body[extkey][f'{extkey}id'] if extkey != 'networkdevice' \
|
||||
else body[extkey]['networkdeviceid']
|
||||
|
||||
updated = client.put(f'{prefix}/{extid}',
|
||||
json={'gaugelabreference': 'GL-2',
|
||||
'maintenancereference': 'MNT-2'},
|
||||
headers=auth_headers)
|
||||
assert updated.status_code == 200, updated.get_json()
|
||||
|
||||
fetched = client.get(f'{prefix}/{extid}', headers=auth_headers).get_json()['data']
|
||||
assert fetched['gaugelabreference'] == 'GL-2'
|
||||
assert fetched['maintenancereference'] == 'MNT-2'
|
||||
|
||||
|
||||
def test_per_type_identifier_seed_count(client, db, auth_headers):
|
||||
"""Seeding produces one boolean setting per identifier x asset type."""
|
||||
response = client.post('/api/settings/seed', headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
from shopdb.core.models import Setting
|
||||
keys = {s.key for s in Setting.query.filter_by(category='identifiers').all()}
|
||||
expected = {
|
||||
f'identifier_{name}_{assettype}_enabled'
|
||||
for name in IDENTIFIER_LABELS
|
||||
for assettype in IDENTIFIER_ASSETTYPES
|
||||
}
|
||||
assert expected <= keys
|
||||
assert len(expected) == len(IDENTIFIER_LABELS) * len(IDENTIFIER_ASSETTYPES)
|
||||
Reference in New Issue
Block a user