124 lines
5.3 KiB
Python
124 lines
5.3 KiB
Python
"""GE-Enforce dashboard card: what is broken on the floor right now.
|
|
|
|
The reports table has always held enforcement failures - the failing entry, its
|
|
exit code, the engine's message - and the only way to see any of it was to open
|
|
one PC's report modal, one PC at a time. A bay returned 500 to every collector
|
|
report for a day and a half before anyone looked. This card is the fix, so
|
|
these tests pin the behaviour that makes it useful rather than decorative.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from plugins.geenforce.models import (ManifestEnforcementReport,
|
|
ManifestEnforcementResult)
|
|
|
|
URL = '/api/geenforce/dashboard/failures'
|
|
|
|
|
|
def _report(db, hostname, iscurrent=True, results=(), scopename='gea-shopfloor-cmm',
|
|
receivedat=None):
|
|
report = ManifestEnforcementReport(
|
|
hostname=hostname, scopename=scopename, phase='runtime',
|
|
status='failed', iscurrent=iscurrent,
|
|
receivedat=receivedat or datetime(2026, 8, 11, 12, 0, 0))
|
|
db.session.add(report)
|
|
db.session.flush()
|
|
for entryname, action, exitcode, message in results:
|
|
db.session.add(ManifestEnforcementResult(
|
|
reportid=report.reportid, entryname=entryname, action=action,
|
|
exitcode=exitcode, message=message))
|
|
db.session.commit()
|
|
return report
|
|
|
|
|
|
def test_a_failed_entry_is_listed_with_what_a_person_needs(client, db, auth_headers):
|
|
"""Per ENTRY, not per report. "Three PCs failed" is a number; "Install
|
|
OpenText failed with exit 1603 on WJSF1234" is something to act on."""
|
|
_report(db, 'WJSF1234', results=[
|
|
('Install OpenText', 'failed', 1603, 'Fatal error during installation')])
|
|
|
|
rows = client.get(URL, headers=auth_headers).get_json()['data']
|
|
assert len(rows) == 1
|
|
assert rows[0]['hostname'] == 'WJSF1234'
|
|
assert rows[0]['entryname'] == 'Install OpenText'
|
|
assert rows[0]['exitcode'] == 1603
|
|
assert 'Fatal error' in rows[0]['message']
|
|
|
|
|
|
def test_only_failures_appear(client, db, auth_headers):
|
|
"""A cycle installs, skips and filters far more than it fails. Listing
|
|
anything but failures is how a card becomes wallpaper."""
|
|
_report(db, 'WJSF1234', results=[
|
|
('Install OpenText', 'failed', 1603, 'boom'),
|
|
('Install Acrobat', 'installed', 0, ''),
|
|
('Set FMS host', 'skipped', 0, ''),
|
|
('CMM settings', 'filtered', 0, 'PCTypes filter')])
|
|
|
|
rows = client.get(URL, headers=auth_headers).get_json()['data']
|
|
assert [r['entryname'] for r in rows] == ['Install OpenText']
|
|
|
|
|
|
def test_a_fixed_failure_disappears_on_its_own(client, db, auth_headers):
|
|
"""Superseded reports are not current. A failure that has since been fixed
|
|
must clear itself rather than needing someone to dismiss it - otherwise the
|
|
card accumulates history and stops meaning "right now"."""
|
|
_report(db, 'WJSF1234', iscurrent=False, results=[
|
|
('Install OpenText', 'failed', 1603, 'boom')])
|
|
_report(db, 'WJSF1234', iscurrent=True, results=[
|
|
('Install OpenText', 'installed', 0, '')])
|
|
|
|
assert client.get(URL, headers=auth_headers).get_json()['data'] == []
|
|
|
|
|
|
def test_the_row_links_to_the_pc_when_shopdb_knows_it(client, db, auth_headers):
|
|
"""A card that names a PC without linking to it is a worse report."""
|
|
from plugins.computers.models import Computer
|
|
from shopdb.core.models import Asset, AssetType
|
|
|
|
atype = AssetType.query.filter_by(assettype='computer').first()
|
|
if not atype:
|
|
atype = AssetType(assettype='computer')
|
|
db.session.add(atype)
|
|
db.session.flush()
|
|
asset = Asset(assetnumber='WJSF1234',
|
|
assettypeid=atype.assettypeid, statusid=1)
|
|
db.session.add(asset)
|
|
db.session.flush()
|
|
comp = Computer(assetid=asset.assetid, hostname='WJSF1234')
|
|
db.session.add(comp)
|
|
db.session.commit()
|
|
|
|
_report(db, 'WJSF1234', results=[('Install OpenText', 'failed', 1603, 'x')])
|
|
rows = client.get(URL, headers=auth_headers).get_json()['data']
|
|
assert rows[0]['computerid'] == comp.computerid
|
|
|
|
|
|
def test_a_pc_shopdb_does_not_know_still_reports_its_failure(client, db,
|
|
auth_headers):
|
|
"""The failure is real even when the inventory is behind. Dropping the row
|
|
would hide exactly the bay most likely to be misconfigured."""
|
|
_report(db, 'GHOSTPC', results=[('Install OpenText', 'failed', 1603, 'x')])
|
|
rows = client.get(URL, headers=auth_headers).get_json()['data']
|
|
assert len(rows) == 1
|
|
assert rows[0]['computerid'] is None
|
|
|
|
|
|
def test_the_card_is_permission_gated(client, db, member_headers):
|
|
"""A dashboard card must not become a way around RBAC."""
|
|
assert client.get(URL, headers=member_headers).status_code == 403
|
|
|
|
|
|
def test_the_widget_declares_data_and_a_renderer_not_a_component(app):
|
|
"""The old contract named a Vue component per widget, which cannot survive
|
|
a lean build - which is why five plugins declared widgets pointing at
|
|
components nobody wrote. A plugin declares data and shape; core renders."""
|
|
from plugins.geenforce.plugin import GeEnforcePlugin
|
|
|
|
widget = GeEnforcePlugin().get_dashboard_widgets()[0]
|
|
assert 'component' not in widget
|
|
assert widget['render'] == 'exceptions'
|
|
assert widget['permission'] == 'geenforce.manage'
|
|
# Empty cards must shrink, or the board becomes wallpaper.
|
|
assert widget['empty'] == 'hide'
|
|
assert widget['map']['link'] == '/pcs/{computerid}'
|