Answer a nonsense filter with a 400, and sweep every list route for the rest
Wave 3. Six findings, one cause: `int(request.args.get('typeid'))` with nothing
catching the ValueError. The caller sent a bad argument and got
500 INTERNAL_ERROR plus a traceback in the log, which says the server is broken
when the truth is that 'abc' is not a number.
shopdb/utils/queryargs.intarg reads it and raises QueryArgError, which an
app-level handler answers as a 400 naming the argument. App-level on purpose: a
route should not have to remember, and forgetting was the entire defect. The
policy differs from get_pagination_params, which falls back to a default for a
bad page number - a nonsense page is harmless and the caller still wants their
list, but silently ignoring a nonsense FILTER returns rows they believe they
excluded.
The gate matters more than the six fixes. tests/test_core/test_bad_query_args.py
sweeps all 131 registered no-parameter GET routes under /api/ with twelve
argument shapes and fails on any 5xx. It found sites the test run had not
reported - statusid and locationid on /api/assets, the second asset-list route,
limit on the employee lookup and the printer supply history - because the
pattern is how a list filter gets written rather than a property of any one
route. Verified non-vacuous by planting the old `int(...)` back into
/api/machines: four tests fail.
Four usb routes are skipped by exact path, with the reason: they reach the
EXTERNAL cmmc_usb database, so their 500 in a test environment is a missing
database rather than an argument fault. That is the complete set - every usb
GET route calling cmmc_usb_connection - not a convenient subset.
One correction worth recording: the first pass rewrote every
`if x := request.args.get(...)` in the touched files, including text arguments,
so a search for 'ZZNOTHINGMATCHES' briefly became a 400. Caught by
test_list_search_identifiers. Only id-shaped arguments are integers.
Scope: product
This commit is contained in:
@@ -13,7 +13,7 @@ from shopdb.api import db, success_response, error_response, paginated_response,
|
||||
from ..models import Notification, NotificationType
|
||||
from ..models.notification import _utc_iso
|
||||
|
||||
from shopdb.api import require_permission, Setting
|
||||
from shopdb.api import require_permission, Setting, intarg
|
||||
|
||||
notifications_bp = Blueprint('notifications', __name__)
|
||||
|
||||
@@ -355,8 +355,8 @@ def list_notifications():
|
||||
query = query.filter(Notification.isactive == True)
|
||||
|
||||
# Type filter
|
||||
if type_id := request.args.get('typeid', request.args.get('type_id')):
|
||||
query = query.filter(Notification.notificationtypeid == int(type_id))
|
||||
if (type_id := intarg('typeid', intarg('type_id'))) is not None:
|
||||
query = query.filter(Notification.notificationtypeid == type_id)
|
||||
|
||||
# Exact-match lookup for idempotent import. Notifications have no strong
|
||||
# natural key; ticketnumber is the best available when a ticket is set.
|
||||
|
||||
Reference in New Issue
Block a user