Remediate the 2026-09-03 feature-test findings
This commit is contained in:
@@ -532,3 +532,123 @@ def test_the_badge_route_is_not_an_enumeration_oracle(client, item, auth_headers
|
||||
assert unknown.status_code == malformed.status_code == 422
|
||||
assert (unknown.get_json()['data']['error']['message']
|
||||
== malformed.get_json()['data']['error']['message'])
|
||||
|
||||
|
||||
# --- the race ---------------------------------------------------------------
|
||||
#
|
||||
# The take read quantityonhand, compared it in Python, and then wrote. Five
|
||||
# concurrent takes of 5 from a bin holding 5 all passed that comparison and all
|
||||
# wrote their own 5 - 5 = 0, so quantityonhand ended up permanently out of step
|
||||
# with the ledger that had recorded every one of them. The comparison now lives
|
||||
# in the UPDATE's WHERE clause, where it is atomic.
|
||||
|
||||
def test_a_take_that_passed_its_precheck_is_still_refused_by_the_row(
|
||||
client, auth_headers, app, item, directory_employee, kiosk,
|
||||
monkeypatch):
|
||||
"""Simulate the race window: the request holds an item that says 5 on hand
|
||||
while the row has already been emptied by somebody else."""
|
||||
from plugins.printedparts.api import routes
|
||||
from plugins.printedparts.models import PrintedItem
|
||||
|
||||
stocked = client.post(f'/api/printedparts/items/{item}/restock',
|
||||
json={'quantity': 5, 'badge': directory_employee},
|
||||
headers=auth_headers)
|
||||
assert stocked.status_code == 200
|
||||
|
||||
real_find = routes._kiosk_find_item
|
||||
|
||||
def stale_find(itemcode):
|
||||
"""What the losing request sees: its own read, taken before the winner
|
||||
committed, still shows a full bin.
|
||||
|
||||
Detached first, so setting the attribute changes only this request's
|
||||
view and never reaches the database - the test is about a stale
|
||||
reader, not about a second writer.
|
||||
"""
|
||||
found = real_find(itemcode)
|
||||
if found is not None:
|
||||
db.session.expunge(found)
|
||||
found.quantityonhand = 5
|
||||
return found
|
||||
|
||||
# Somebody else empties the bin first, through the kiosk, so the ledger
|
||||
# records it the way a real competing take would.
|
||||
winner = client.post('/api/printedparts/kiosk/take',
|
||||
json={'itemcode': '3DP-9001', 'quantity': 5,
|
||||
'badge': directory_employee},
|
||||
headers=KIOSK_HEADERS)
|
||||
assert winner.status_code == 200, winner.get_json()
|
||||
|
||||
monkeypatch.setattr(routes, '_kiosk_find_item', stale_find)
|
||||
taken = client.post('/api/printedparts/kiosk/take',
|
||||
json={'itemcode': '3DP-9001', 'quantity': 5,
|
||||
'badge': directory_employee},
|
||||
headers=KIOSK_HEADERS)
|
||||
assert taken.status_code == 400, taken.get_json()
|
||||
assert 'on hand' in taken.get_json()['data']['error']['message']
|
||||
|
||||
with app.app_context():
|
||||
after = db.session.query(PrintedItem).filter_by(
|
||||
printeditemid=item).first()
|
||||
assert after.quantityonhand == 0, 'stock must not go negative'
|
||||
ledger = sum(t.quantitychange for t in after.transactions)
|
||||
assert ledger == after.quantityonhand, \
|
||||
'quantityonhand and the ledger must agree'
|
||||
|
||||
|
||||
# --- the number on the bin is the number you type ---------------------------
|
||||
#
|
||||
# Keypad entry matched the digits ANYWHERE in the code, and the shipped prefix
|
||||
# contains one: '3DP0004' collapsed to 30004, so an operator typing the 4
|
||||
# printed on the label was told no part matches that barcode, and had to work
|
||||
# out that the leading 3 belonged to the prefix.
|
||||
|
||||
def test_the_keypad_matches_the_number_printed_on_the_label(client, db,
|
||||
auth_headers,
|
||||
kiosk,
|
||||
directory_employee):
|
||||
from plugins.printedparts.models import PrintedItem
|
||||
|
||||
created = client.post('/api/printedparts/items', headers=auth_headers,
|
||||
json={'itemname': 'Keypad clip'})
|
||||
assert created.status_code == 201, created.get_json()
|
||||
itemcode = created.get_json()['data']['itemcode']
|
||||
assert itemcode.startswith('3DP'), itemcode
|
||||
|
||||
number = str(int(itemcode[3:]))
|
||||
found = client.get('/api/printedparts/kiosk/item/%s' % number,
|
||||
headers=KIOSK_HEADERS)
|
||||
assert found.status_code == 200, (number, found.get_json())
|
||||
assert found.get_json()['data']['itemcode'] == itemcode
|
||||
|
||||
|
||||
def test_the_full_code_still_works(client, db, auth_headers, kiosk):
|
||||
created = client.post('/api/printedparts/items', headers=auth_headers,
|
||||
json={'itemname': 'Full code clip'})
|
||||
itemcode = created.get_json()['data']['itemcode']
|
||||
|
||||
found = client.get('/api/printedparts/kiosk/item/%s' % itemcode,
|
||||
headers=KIOSK_HEADERS)
|
||||
assert found.status_code == 200, found.get_json()
|
||||
|
||||
|
||||
def test_an_ambiguous_number_is_still_refused(client, db, auth_headers, kiosk):
|
||||
"""Two items whose trailing numbers agree must not resolve to a guess -
|
||||
taking from the wrong bin is worse than being told to scan the label."""
|
||||
from plugins.printedparts.models import PrintedItem
|
||||
|
||||
first = client.post('/api/printedparts/items', headers=auth_headers,
|
||||
json={'itemname': 'Ambiguous one'}).get_json()['data']
|
||||
item = db.session.get(PrintedItem, first['printeditemid'])
|
||||
item.gagelabtag = 'WJRP%04d' % 7777
|
||||
db.session.commit()
|
||||
|
||||
second = client.post('/api/printedparts/items', headers=auth_headers,
|
||||
json={'itemname': 'Ambiguous two'}).get_json()['data']
|
||||
other = db.session.get(PrintedItem, second['printeditemid'])
|
||||
other.gagelabtag = 'GAGE%04d' % 7777
|
||||
db.session.commit()
|
||||
|
||||
refused = client.get('/api/printedparts/kiosk/item/7777',
|
||||
headers=KIOSK_HEADERS)
|
||||
assert refused.status_code == 404, refused.get_json()
|
||||
|
||||
Reference in New Issue
Block a user