Remediate the 2026-09-03 feature-test findings
This commit is contained in:
@@ -314,3 +314,126 @@ def test_the_token_alone_is_not_enough_when_the_kiosk_is_off(client, kiosk):
|
||||
_setting(kiosk, 'usb_kiosk_enabled', 'false')
|
||||
assert client.get('/api/usb/kiosk/device/USB-1',
|
||||
headers=KIOSK_HEADERS).status_code == 404
|
||||
|
||||
|
||||
# --- the race, and who returned it ------------------------------------------
|
||||
#
|
||||
# The kiosk decided "is this drive available?" in Python and then wrote. Two
|
||||
# scans of the same drive that interleaved both read "available", so both
|
||||
# inserted a custody row: one drive recorded out to two people, and the
|
||||
# documented 409 never fired. The fix puts ischeckedout in the WHERE clause, so
|
||||
# the database settles it at write time.
|
||||
|
||||
def test_a_checkout_that_passed_its_availability_check_is_still_refused(
|
||||
client, kiosk, monkeypatch):
|
||||
"""Simulate the race window: the request holds a device object that says
|
||||
available while the row has already been claimed by somebody else."""
|
||||
from plugins.usb.api import selfhosted
|
||||
from plugins.usb.models import USBDevice
|
||||
|
||||
first = _out(client, badge='270015376')
|
||||
assert first.status_code == 200
|
||||
|
||||
# What the losing request sees: its own read, taken before the winner
|
||||
# committed, still says the drive is on the shelf.
|
||||
stale = USBDevice.query.filter_by(serialnumber='USB-1').first()
|
||||
monkeypatch.setattr(
|
||||
selfhosted, '_find',
|
||||
lambda device_id: _stale_copy(stale))
|
||||
|
||||
second = _out(client, badge='270099999')
|
||||
assert second.status_code == 409, second.get_json()
|
||||
|
||||
open_rows = _open_checkouts(USBDevice)
|
||||
assert len(open_rows) == 1, 'one drive, one open custody row'
|
||||
assert open_rows[0].sso == '270015376'
|
||||
|
||||
|
||||
def _stale_copy(device):
|
||||
"""A detached stand-in that still believes the drive is available."""
|
||||
class Stale:
|
||||
usbdeviceid = device.usbdeviceid
|
||||
serialnumber = device.serialnumber
|
||||
isactive = True
|
||||
ischeckedout = False
|
||||
currentuserid = None
|
||||
currentusername = None
|
||||
return Stale()
|
||||
|
||||
|
||||
def _open_checkouts(_):
|
||||
from plugins.usb.models import USBCheckout
|
||||
return USBCheckout.query.filter_by(checkintime=None).all()
|
||||
|
||||
|
||||
def test_a_return_records_who_handed_it_back_not_only_the_holder(client, kiosk):
|
||||
"""Returning for a colleague is a supported flow, so the check-in has to
|
||||
name the person who actually walked it back."""
|
||||
from plugins.usb.models import USBCheckout
|
||||
|
||||
assert _out(client, badge='270015376').status_code == 200
|
||||
returned = _in(client, badge='270099999', forsomeoneelse=True)
|
||||
assert returned.status_code == 200, returned.get_json()
|
||||
|
||||
row = USBCheckout.query.order_by(USBCheckout.checkoutid.desc()).first()
|
||||
assert row.sso == '270015376', 'the holder still owns the checkout'
|
||||
assert row.checkinsso == '270099999', 'the returner is recorded'
|
||||
assert row.checkinname and 'Sam' in row.checkinname
|
||||
|
||||
|
||||
def test_a_check_in_closes_every_open_row_for_that_device(client, kiosk, db):
|
||||
"""Defence in depth for history written before the guard existed: a device
|
||||
can only be out once, so a second open row is corruption, and closing only
|
||||
the newest left the older ones open forever."""
|
||||
from plugins.usb.models import USBCheckout, USBDevice
|
||||
|
||||
assert _out(client, badge='270015376').status_code == 200
|
||||
device = USBDevice.query.filter_by(serialnumber='USB-1').first()
|
||||
db.session.add(USBCheckout(usbdeviceid=device.usbdeviceid, sso='270099999',
|
||||
checkoutname='Sam Okafor'))
|
||||
db.session.commit()
|
||||
assert len(_open_checkouts(USBDevice)) == 2
|
||||
|
||||
assert _in(client, badge='270015376').status_code == 200
|
||||
assert _open_checkouts(USBDevice) == []
|
||||
|
||||
|
||||
# --- one badge vocabulary for the whole site --------------------------------
|
||||
#
|
||||
# The parts kiosk understood both shapes a scanner emits; this one took the
|
||||
# string raw and compared it to an SSO. So an operator whose keyboard-wedge
|
||||
# scanner emits the PayNo wrapper could take a 3D-printed part and could not
|
||||
# check out a drive, with the same badge, at two cabinets a few metres apart.
|
||||
#
|
||||
# The same change closed a subtler hole: str.isdigit() is true for Arabic-Indic
|
||||
# digits, so a badge written in them resolved through int() to a REAL employee
|
||||
# and was then stored as an SSO string that joins to nothing.
|
||||
|
||||
def test_the_payno_wrapper_works_at_this_kiosk_too(client, kiosk):
|
||||
from plugins.usb.models import USBCheckout
|
||||
|
||||
response = _out(client, badge='0270015376BZ')
|
||||
assert response.status_code == 200, response.get_json()
|
||||
|
||||
row = USBCheckout.query.order_by(USBCheckout.checkoutid.desc()).first()
|
||||
assert row.sso == '270015376', \
|
||||
'the custody row records the SSO, not the wrapper it arrived in'
|
||||
assert 'Dana' in (row.checkoutname or '')
|
||||
|
||||
|
||||
def test_a_badge_in_non_ascii_digits_is_refused(client, kiosk):
|
||||
"""It looks like a number to str.isdigit() and parses through int(), so it
|
||||
resolved to a real employee under an SSO nothing else matches."""
|
||||
# Written as escapes so this file stays ASCII, which the style gate
|
||||
# enforces: these are Arabic-Indic digits, U+0669 and friends.
|
||||
arabic_indic = '\u0669\u0667\u0660\u0660\u0660\u0667\u0661'
|
||||
response = _out(client, badge=arabic_indic)
|
||||
assert response.status_code == 422, response.get_json()
|
||||
|
||||
|
||||
def test_the_same_person_scanning_either_shape_is_one_holder(client, kiosk):
|
||||
"""Wrapper out, bare number back: the check-in has to recognise it as the
|
||||
same person, or the drive can never be returned by its holder."""
|
||||
assert _out(client, badge='0270015376BZ').status_code == 200
|
||||
returned = _in(client, badge='270015376')
|
||||
assert returned.status_code == 200, returned.get_json()
|
||||
|
||||
Reference in New Issue
Block a user