From c8360c5f2594db6851d1c7311014ee15ebd9965a Mon Sep 17 00:00:00 2001 From: Nano Taboada <87288+nanotaboada@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:51:30 -0300 Subject: [PATCH] feat(api): enforce squad number immutability on PUT (#529) Add a mismatch guard in put_async: if squad_number in the request body does not match the path parameter, return HTTP 400 Bad Request. The path parameter is the authoritative source of identity on PUT. Document the single-model design decision in PlayerRequestModel: one model intentionally covers both POST and PUT, with per-operation differences handled at the route layer. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Claude --- models/player_model.py | 9 +++++++++ routes/player_route.py | 5 +++++ tests/test_main.py | 14 ++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/models/player_model.py b/models/player_model.py index 100cb39..aa85389 100644 --- a/models/player_model.py +++ b/models/player_model.py @@ -5,6 +5,15 @@ - `PlayerRequestModel`: Represents player data for Create and Update operations. - `PlayerResponseModel`: Represents player data including UUID for Retrieve operations. +Design decision — single request model vs split models: + A single `PlayerRequestModel` is intentionally shared by both POST (Create) + and PUT (Update). Per-operation differences are handled at the route layer + rather than by duplicating the model: + - POST checks that `squad_number` does not already exist (→ 409 Conflict). + - PUT checks that `squad_number` in the body matches the path parameter + (→ 400 Bad Request), ensuring the request is unambiguous. The path + parameter is always the authoritative source of identity on PUT. + These models are used for data validation and serialization in the API. """ diff --git a/routes/player_route.py b/routes/player_route.py index 4e50d66..631e511 100644 --- a/routes/player_route.py +++ b/routes/player_route.py @@ -199,9 +199,14 @@ async def put_async( async_session (AsyncSession): The async version of a SQLAlchemy ORM session. Raises: + HTTPException: HTTP 400 Bad Request if squad_number in the request body does + not match the path parameter. The path parameter is the authoritative source + of identity on PUT; a mismatch makes the request ambiguous. HTTPException: HTTP 404 Not Found error if the Player with the specified Squad Number does not exist. """ + if player_model.squad_number != squad_number: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST) player = await player_service.retrieve_by_squad_number_async( async_session, squad_number ) diff --git a/tests/test_main.py b/tests/test_main.py index 6ac99be..9ed2f73 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -234,6 +234,20 @@ def test_request_put_player_squadnumber_existing_response_status_no_content(clie assert response.status_code == 204 +def test_request_put_player_squadnumber_mismatch_response_status_bad_request(client): + """PUT /players/squadnumber/{squad_number} with mismatched squad number in body returns 400 Bad Request""" + # Arrange + squad_number = existing_player().squad_number + player = existing_player() + player.squad_number = unknown_player().squad_number + # Act + response = client.put( + PATH + "squadnumber/" + str(squad_number), json=player.__dict__ + ) + # Assert + assert response.status_code == 400 + + # DELETE /players/squadnumber/{squad_number} -----------------------------------