Plugin reports render; USB check-in/out audit report; kiosk drive lists
This commit is contained in:
139
tests/test_plugins/test_usb_reports.py
Normal file
139
tests/test_plugins/test_usb_reports.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""USB check-in / check-out audit report.
|
||||
|
||||
CMMC removable-media tracking is an audit obligation, and the question an audit
|
||||
asks is "who had this drive, when" - which a total cannot answer. So this
|
||||
reports EVENTS over a window, with the window named rather than expressed as a
|
||||
day count nobody should have to work out.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from shopdb.core.models import Setting
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def usbdata(db):
|
||||
"""Self-hosted USB with a loan returned today and one from long ago."""
|
||||
from plugins.usb.models import USBDevice, USBCheckout
|
||||
|
||||
db.session.add(Setting(key='usb_directory_mode', value='selfhosted',
|
||||
valuetype='string', category='site'))
|
||||
recent = USBDevice(serialnumber='USB-1', label='Blue Kingston')
|
||||
old = USBDevice(serialnumber='USB-2', label='Red SanDisk')
|
||||
db.session.add_all([recent, old])
|
||||
db.session.flush()
|
||||
|
||||
# Taken and returned today: two events.
|
||||
db.session.add(USBCheckout(usbdeviceid=recent.usbdeviceid, sso='270015376',
|
||||
checkoutname='Dana Reyes',
|
||||
checkouttime=_now() - timedelta(hours=3),
|
||||
checkintime=_now() - timedelta(hours=1)))
|
||||
# Taken 200 days ago and never returned: one event, outside most windows.
|
||||
db.session.add(USBCheckout(usbdeviceid=old.usbdeviceid, sso='270099999',
|
||||
checkoutname='Sam Okafor',
|
||||
checkouttime=_now() - timedelta(days=200)))
|
||||
db.session.commit()
|
||||
return db
|
||||
|
||||
|
||||
def _report(client, headers, **params):
|
||||
query = '&'.join(f'{k}={v}' for k, v in params.items())
|
||||
response = client.get(f'/api/usb/reports/audit?{query}', headers=headers)
|
||||
assert response.status_code == 200, response.get_json()
|
||||
return response.get_json()['data']
|
||||
|
||||
|
||||
def test_a_loan_produces_both_a_take_and_a_return(client, usbdata, auth_headers):
|
||||
"""One stored row holds both ends of a loan.
|
||||
|
||||
Reporting it once would make a returned drive look identical to one still
|
||||
out, which is the opposite of what the log is for.
|
||||
"""
|
||||
data = _report(client, auth_headers, period='month')
|
||||
actions = sorted(row['action'] for row in data['rows'])
|
||||
|
||||
assert actions == ['check-in', 'check-out']
|
||||
assert data['summary']['checkouts'] == 1
|
||||
assert data['summary']['checkins'] == 1
|
||||
|
||||
|
||||
def test_the_window_actually_bounds_the_rows(client, usbdata, auth_headers):
|
||||
month = _report(client, auth_headers, period='month')
|
||||
year = _report(client, auth_headers, period='year')
|
||||
|
||||
assert {row['device_id'] for row in month['rows']} == {'USB-1'}
|
||||
assert {row['device_id'] for row in year['rows']} == {'USB-1', 'USB-2'}
|
||||
|
||||
|
||||
def test_all_time_keeps_everything(client, usbdata, auth_headers):
|
||||
data = _report(client, auth_headers, period='all')
|
||||
assert data['days'] == 0
|
||||
assert data['summary']['events'] == 3
|
||||
|
||||
|
||||
def test_today_is_a_window_too(client, usbdata, auth_headers):
|
||||
"""A shift handover asks what happened today, not this month."""
|
||||
data = _report(client, auth_headers, period='day')
|
||||
assert data['days'] == 1
|
||||
assert data['summary']['events'] == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize('period,days', [('day', 1), ('month', 30),
|
||||
('sixmonths', 182), ('year', 365),
|
||||
('all', 0)])
|
||||
def test_every_named_window_maps_to_a_day_count(client, usbdata, auth_headers,
|
||||
period, days):
|
||||
assert _report(client, auth_headers, period=period)['days'] == days
|
||||
|
||||
|
||||
def test_an_unknown_period_falls_back_rather_than_failing(client, usbdata,
|
||||
auth_headers):
|
||||
"""A bad query string should not 500 a report somebody is mid-audit on."""
|
||||
assert _report(client, auth_headers, period='fortnight')['days'] == 30
|
||||
|
||||
|
||||
def test_rows_are_newest_first(client, usbdata, auth_headers):
|
||||
rows = _report(client, auth_headers, period='all')['rows']
|
||||
stamps = [row['timestamp'] for row in rows]
|
||||
assert stamps == sorted(stamps, reverse=True)
|
||||
|
||||
|
||||
def test_it_says_who_and_which_drive(client, usbdata, auth_headers):
|
||||
rows = _report(client, auth_headers, period='month')['rows']
|
||||
row = rows[0]
|
||||
assert row['name'] == 'Dana Reyes'
|
||||
assert row['badge'] == '270015376'
|
||||
assert row['device_id'] == 'USB-1'
|
||||
assert row['device_desc'] == 'Blue Kingston'
|
||||
|
||||
|
||||
def test_it_exports_as_csv(client, usbdata, auth_headers):
|
||||
response = client.get('/api/usb/reports/audit?period=all&format=csv',
|
||||
headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
assert response.mimetype == 'text/csv'
|
||||
body = response.get_data(as_text=True)
|
||||
assert body.splitlines()[0].startswith('timestamp,action,device_id')
|
||||
assert 'Dana Reyes' in body
|
||||
|
||||
|
||||
def test_reading_the_audit_needs_permission(client, usbdata, member_headers):
|
||||
"""A role-less member cannot read who had which drive."""
|
||||
response = client.get('/api/usb/reports/audit?period=all',
|
||||
headers=member_headers)
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_the_report_is_registered_so_it_appears_in_the_catalogue(client, usbdata,
|
||||
auth_headers):
|
||||
from plugins.usb.plugin import USBPlugin
|
||||
report = USBPlugin().get_reports()[0]
|
||||
assert report['id'] == 'usb-audit'
|
||||
# The endpoint is what the reports page fetches; a wrong one renders blank.
|
||||
assert report['endpoint'] == '/api/usb/reports/audit'
|
||||
Reference in New Issue
Block a user