279 lines
11 KiB
Python
279 lines
11 KiB
Python
"""The USB kiosk: an UNAUTHENTICATED write, so the guards are the feature.
|
|
|
|
The kiosk exists so somebody at the drive cabinet - with no ShopDB login - can
|
|
take a drive and put it back, with the action attributed to their badge instead
|
|
of to a signed-in operator. Everything here is about the bar that opens the door
|
|
only as far as it has to: it can move a device between checked-out and
|
|
checked-in and nothing else, it refuses a badge it cannot put a name to, and it
|
|
does not exist at all until a site turns it on.
|
|
|
|
Self-hosted mode. The external cmmc_usb path is the same contract against raw
|
|
SQL and needs that database to exercise.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from shopdb.core.models import Setting
|
|
|
|
|
|
@pytest.fixture
|
|
def kiosk(db):
|
|
"""Self-hosted USB, kiosk ON, one employee and one available drive."""
|
|
from plugins.employees.models import DirectoryEmployee
|
|
from plugins.usb.models import USBDevice
|
|
|
|
db.session.add(Setting(key='usb_directory_mode', value='selfhosted',
|
|
valuetype='string', category='site'))
|
|
db.session.add(Setting(key='usb_kiosk_enabled', value='true',
|
|
valuetype='boolean', category='usb'))
|
|
db.session.add(DirectoryEmployee(sso=270015376, firstname='Dana',
|
|
lastname='Reyes'))
|
|
db.session.add(DirectoryEmployee(sso=270099999, firstname='Sam',
|
|
lastname='Okafor'))
|
|
db.session.add(USBDevice(serialnumber='USB-1', label='Blue Kingston',
|
|
ischeckedout=False))
|
|
db.session.commit()
|
|
return db
|
|
|
|
|
|
def _setting(db, key, value):
|
|
row = Setting.query.filter_by(key=key).first()
|
|
if row is None:
|
|
db.session.add(Setting(key=key, value=value, valuetype='boolean',
|
|
category='usb'))
|
|
else:
|
|
row.value = value
|
|
db.session.commit()
|
|
|
|
|
|
def _out(client, badge='270015376', **extra):
|
|
return client.post('/api/usb/kiosk/checkout',
|
|
json=dict({'device_id': 'USB-1', 'badge': badge}, **extra))
|
|
|
|
|
|
def _in(client, badge='270015376', **extra):
|
|
return client.post('/api/usb/kiosk/checkin',
|
|
json=dict({'device_id': 'USB-1', 'badge': badge}, **extra))
|
|
|
|
|
|
# --- the door itself --------------------------------------------------------
|
|
|
|
def test_the_kiosk_does_not_exist_until_a_site_turns_it_on(client, kiosk):
|
|
"""Default off, and 404 rather than 403.
|
|
|
|
An unauthenticated write is a decision about the door. A site that has not
|
|
made that decision should not have these routes answering at all, and a
|
|
disabled kiosk should not advertise that there is a door here.
|
|
"""
|
|
_setting(kiosk, 'usb_kiosk_enabled', 'false')
|
|
|
|
assert client.get('/api/usb/kiosk/device/USB-1').status_code == 404
|
|
assert _out(client).status_code == 404
|
|
assert _in(client).status_code == 404
|
|
|
|
|
|
def test_it_ships_off(app):
|
|
"""The declared default is what a fresh site gets."""
|
|
from plugins.usb.plugin import USBPlugin
|
|
defaults = {s['key']: s for s in USBPlugin().get_settings_defaults()}
|
|
assert defaults['usb_kiosk_enabled']['value'] == 'false'
|
|
|
|
|
|
def test_no_login_is_required(client, kiosk):
|
|
"""The whole point: no Authorization header anywhere in this file."""
|
|
response = client.get('/api/usb/kiosk/device/USB-1')
|
|
assert response.status_code == 200
|
|
assert response.get_json()['data']['device_id'] == 'USB-1'
|
|
|
|
|
|
def test_the_kiosk_cannot_create_edit_or_delete(client, kiosk):
|
|
"""It moves a device between two states. It is not an admin surface."""
|
|
assert client.post('/api/usb/kiosk/device/USB-1').status_code in (404, 405)
|
|
assert client.delete('/api/usb/kiosk/device/USB-1').status_code in (404, 405)
|
|
# And the authenticated write routes are still gated.
|
|
assert client.post('/api/usb/USB-1/checkout',
|
|
json={'badge': '270015376'}).status_code == 401
|
|
|
|
|
|
# --- badges -----------------------------------------------------------------
|
|
|
|
def test_a_badge_it_cannot_name_is_refused(client, kiosk):
|
|
"""A checkout nobody can trace to a person defeats the tracking.
|
|
|
|
The signed-in route creates the user row for an unknown badge; the kiosk
|
|
must not, because there is no operator standing there to vouch for it.
|
|
"""
|
|
response = _out(client, badge='999999999')
|
|
assert response.status_code == 422
|
|
assert 'not recognized' in response.get_json()['data']['error']['message']
|
|
|
|
from plugins.usb.models import USBDevice
|
|
assert USBDevice.query.filter_by(serialnumber='USB-1').first().ischeckedout is False
|
|
|
|
|
|
def test_a_missing_badge_is_refused(client, kiosk):
|
|
assert client.post('/api/usb/kiosk/checkout',
|
|
json={'device_id': 'USB-1'}).status_code == 400
|
|
|
|
|
|
# --- taking one -------------------------------------------------------------
|
|
|
|
def test_taking_a_drive_records_who_took_it(client, kiosk):
|
|
from plugins.usb.models import USBDevice
|
|
|
|
response = _out(client)
|
|
assert response.status_code == 200, response.get_json()
|
|
|
|
device = USBDevice.query.filter_by(serialnumber='USB-1').first()
|
|
assert device.ischeckedout is True
|
|
assert device.currentuserid == '270015376'
|
|
assert device.currentusername == 'Dana Reyes'
|
|
|
|
|
|
def test_a_drive_already_out_cannot_be_taken_again(client, kiosk):
|
|
_out(client)
|
|
response = _out(client, badge='270099999')
|
|
assert response.status_code == 409
|
|
assert 'Dana Reyes' in response.get_json()['data']['error']['message']
|
|
|
|
|
|
def test_an_unknown_code_is_a_404(client, kiosk):
|
|
response = client.post('/api/usb/kiosk/checkout',
|
|
json={'device_id': 'NOPE', 'badge': '270015376'})
|
|
assert response.status_code == 404
|
|
|
|
|
|
# --- putting one back -------------------------------------------------------
|
|
|
|
def test_the_holder_returns_their_own_drive(client, kiosk):
|
|
from plugins.usb.models import USBDevice
|
|
_out(client)
|
|
|
|
assert _in(client).status_code == 200
|
|
device = USBDevice.query.filter_by(serialnumber='USB-1').first()
|
|
assert device.ischeckedout is False
|
|
assert device.currentuserid is None
|
|
|
|
|
|
def test_returning_somebody_elses_drive_asks_first(client, kiosk):
|
|
"""Not a silent success: the record is about to say they handed it back."""
|
|
from plugins.usb.models import USBDevice
|
|
_out(client)
|
|
|
|
response = _in(client, badge='270099999')
|
|
assert response.status_code == 409
|
|
details = response.get_json()['data']['error']['details']
|
|
assert details['needsconfirm'] is True
|
|
assert details['holdername'] == 'Dana Reyes'
|
|
assert USBDevice.query.filter_by(serialnumber='USB-1').first().ischeckedout is True
|
|
|
|
|
|
def test_confirming_returns_it_for_them(client, kiosk):
|
|
"""A drive left on a bench by somebody on leave still gets put back.
|
|
|
|
Refusing outright just moves the problem to a person with a login.
|
|
"""
|
|
from plugins.usb.models import USBDevice
|
|
_out(client)
|
|
|
|
response = _in(client, badge='270099999', forsomeoneelse=True)
|
|
assert response.status_code == 200, response.get_json()
|
|
assert USBDevice.query.filter_by(serialnumber='USB-1').first().ischeckedout is False
|
|
|
|
|
|
def test_a_site_can_refuse_returns_for_others_outright(client, kiosk):
|
|
_setting(kiosk, 'usb_kiosk_allow_return_for_others', 'false')
|
|
_out(client)
|
|
|
|
response = _in(client, badge='270099999', forsomeoneelse=True)
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_a_drive_that_is_not_out_cannot_be_returned(client, kiosk):
|
|
assert _in(client).status_code == 400
|
|
|
|
|
|
# --- what the screen is allowed to know -------------------------------------
|
|
|
|
def test_the_lookup_names_the_holder_but_not_their_badge(client, kiosk):
|
|
"""The confirm step has to show whose it is; it does not need their SSO."""
|
|
_out(client)
|
|
data = client.get('/api/usb/kiosk/device/USB-1').get_json()['data']
|
|
|
|
assert data['holdername'] == 'Dana Reyes'
|
|
assert '270015376' not in str(data)
|
|
|
|
|
|
# --- the picker -------------------------------------------------------------
|
|
# The drives carry no barcode, so the kiosk cannot ask somebody to present one -
|
|
# it has to offer a LIST. That is a real widening of what a logged-out screen
|
|
# exposes, so the list is not anonymous: it costs a badge that resolves to a
|
|
# real person, the same bar taking a drive already had to clear.
|
|
|
|
def _session(client, badge='270015376'):
|
|
return client.post('/api/usb/kiosk/session', json={'badge': badge})
|
|
|
|
|
|
def test_the_device_list_costs_a_valid_badge(client, kiosk):
|
|
assert client.post('/api/usb/kiosk/session', json={}).status_code == 400
|
|
assert _session(client, badge='999999999').status_code == 422
|
|
|
|
|
|
def test_the_list_is_not_reachable_when_the_kiosk_is_off(client, kiosk):
|
|
_setting(kiosk, 'usb_kiosk_enabled', 'false')
|
|
assert _session(client).status_code == 404
|
|
assert client.post('/api/usb/kiosk/others',
|
|
json={'badge': '270015376'}).status_code == 404
|
|
|
|
|
|
def test_the_session_separates_yours_from_available(client, kiosk):
|
|
from plugins.usb.models import USBDevice
|
|
kiosk.session.add(USBDevice(serialnumber='USB-2', label='Second',
|
|
ischeckedout=False))
|
|
kiosk.session.commit()
|
|
_out(client) # 270015376 takes USB-1
|
|
|
|
data = _session(client).get_json()['data']
|
|
assert data['name'] == 'Dana Reyes'
|
|
assert [d['device_id'] for d in data['yours']] == ['USB-1']
|
|
assert [d['device_id'] for d in data['available']] == ['USB-2']
|
|
assert data['otherscount'] == 0
|
|
|
|
|
|
def test_somebody_elses_drive_is_a_count_not_a_name(client, kiosk):
|
|
"""A passer-by should not read the cabinet's holders off a wall screen."""
|
|
_out(client) # Dana takes USB-1
|
|
|
|
data = _session(client, badge='270099999').get_json()['data']
|
|
assert data['yours'] == []
|
|
assert data['otherscount'] == 1
|
|
assert 'Dana' not in str(data)
|
|
|
|
|
|
def test_names_appear_only_on_the_deliberate_second_step(client, kiosk):
|
|
_out(client)
|
|
|
|
rows = client.post('/api/usb/kiosk/others',
|
|
json={'badge': '270099999'}).get_json()['data']
|
|
assert [r['device_id'] for r in rows] == ['USB-1']
|
|
assert rows[0]['holdername'] == 'Dana Reyes'
|
|
|
|
|
|
def test_the_others_list_is_gone_when_returns_for_others_are_refused(client, kiosk):
|
|
"""No point showing a list of drives the kiosk will not let anyone return."""
|
|
_setting(kiosk, 'usb_kiosk_allow_return_for_others', 'false')
|
|
_out(client)
|
|
|
|
assert client.post('/api/usb/kiosk/others',
|
|
json={'badge': '270099999'}).status_code == 404
|
|
|
|
|
|
def test_a_retired_drive_is_not_offered(client, kiosk):
|
|
from plugins.usb.models import USBDevice
|
|
device = USBDevice.query.filter_by(serialnumber='USB-1').first()
|
|
device.isactive = False
|
|
kiosk.session.commit()
|
|
|
|
data = _session(client).get_json()['data']
|
|
assert data['available'] == []
|