From 7f31ff49ff656c8353ee98333f54b3dd3770dabd Mon Sep 17 00:00:00 2001 From: fullsend-code <278716306+fullsend-ai-coder[bot]@users.noreply.github.com> Date: Fri, 21 Aug 2026 19:09:18 +0000 Subject: [PATCH] fix(#28): reject unknown fields in POST and PATCH /items endpoints Both create_item() and update_item() silently dropped unknown JSON fields while returning HTTP 200, causing callers to believe updates persisted when they did not. This is a data-loss risk for clients sending fields like warehouse_bin that the API does not recognize. Add allowed-field validation to both endpoints: POST /items accepts only {"name"}, PATCH /items/:id accepts only {"name", "done"}. Requests with unknown fields now return HTTP 400 with an error message listing the unrecognized field names. Add tests covering unknown-only fields, mixed known/unknown fields, and valid-only fields for both endpoints. Note: pre-existing ruff lint errors (I001 import sorting, UP017 timezone.utc alias) exist in unchanged lines and are not addressed by this change. Closes #28 --- app.py | 10 ++++++++++ test_app.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/app.py b/app.py index a2d2561..eb74fa2 100644 --- a/app.py +++ b/app.py @@ -26,6 +26,11 @@ def create_item(): if not data or "name" not in data: return jsonify(error="name is required"), 400 + allowed = {"name"} + unknown = set(data.keys()) - allowed + if unknown: + return jsonify(error=f"Unknown fields: {', '.join(sorted(unknown))}"), 400 + item = { "id": len(_get_items()) + 1, "name": data["name"], @@ -43,6 +48,11 @@ def update_item(item_id): return jsonify(error="item not found"), 404 data = request.get_json() + allowed = {"name", "done"} + unknown = set(data.keys()) - allowed + if unknown: + return jsonify(error=f"Unknown fields: {', '.join(sorted(unknown))}"), 400 + if "done" in data: item["done"] = data["done"] if "name" in data: diff --git a/test_app.py b/test_app.py index d5ae8f6..2e88fb4 100644 --- a/test_app.py +++ b/test_app.py @@ -63,3 +63,38 @@ def test_delete_item(client): def test_delete_item_not_found(client): resp = client.delete("/items/999") assert resp.status_code == 404 + + +def test_patch_unknown_fields_rejected(client): + client.post("/items", json={"name": "widget"}) + resp = client.patch("/items/1", json={"warehouse_bin": "A-12"}) + assert resp.status_code == 400 + assert "warehouse_bin" in resp.get_json()["error"] + + +def test_patch_mixed_known_and_unknown_fields_rejected(client): + client.post("/items", json={"name": "widget"}) + resp = client.patch("/items/1", json={"done": True, "warehouse_bin": "A-12"}) + assert resp.status_code == 400 + assert "warehouse_bin" in resp.get_json()["error"] + + +def test_patch_valid_fields_accepted(client): + client.post("/items", json={"name": "widget"}) + resp = client.patch("/items/1", json={"name": "gadget", "done": True}) + assert resp.status_code == 200 + data = resp.get_json() + assert data["name"] == "gadget" + assert data["done"] is True + + +def test_create_item_unknown_fields_rejected(client): + resp = client.post("/items", json={"name": "widget", "warehouse_bin": "A-12"}) + assert resp.status_code == 400 + assert "warehouse_bin" in resp.get_json()["error"] + + +def test_create_item_valid_fields_accepted(client): + resp = client.post("/items", json={"name": "widget"}) + assert resp.status_code == 201 + assert resp.get_json()["name"] == "widget"