Repository navigation
[2C-03] Make notification response endpoint idempotent on (notification_id, response) tuple - #210
Merged
Merged
Conversation
…note) (#202) Per [2C-03]. The /api/notifications/{id}/respond endpoint now returns 200 with the existing row when the same response + response_note are re-posted (no DB write, responded_at unchanged), and 409 only when the retry diverges (different response, or same response with a different note). Pending stays 409, expired stays 410, not-found stays 404 — those branches are unchanged. Adds TestRespondIdempotency with the three acceptance-criteria cases plus a no-note variant. Existing test_409_already_responded was renamed and updated to use a different response on the second call, since the original test embodied the pre-idempotency contract.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #202
Verification
ruff check .— ✅ Pass (All checks passed!)pytest -v— ✅ Pass (1362 passed)Ran locally against develop HEAD at
2ec8521immediately before opening this PR.Summary
POST /api/notifications/{id}/respondis now idempotent on the(notification_id, response, response_note)tuple. A retry thatmatches the recorded response and note returns 200 with the existing
row (no DB write,
responded_atunchanged). A retry with a differentresponse, or the same response with a different note, returns 409 —
that's a legitimate conflict (the user tapped a different action a
second time). The
pending(409),expired(410), andnot-found(404) branches are unchanged.
Changes
app/routers/notification.py—respond_to_notificationnowshort-circuits the
respondedbranch when the new payload'sresponseandresponse_notematch the recorded values, returningthe existing notification with no write. Docstring documents the
idempotency contract and the client-facing implication that 200
means "applied" regardless of first-vs-retry.
tests/test_notification_respond.py— addsTestRespondIdempotencywith the three acceptance-criteria cases plus a no-note variant
(covers idempotency when
response_noteis null on both sides).Renames and updates the previous
test_409_already_responded(seeDeviations).
How to Verify
developHEAD2ec8521.POST /respondwith{"response": "Done", "response_note": "Got it"}. Observe 200 andcapture the
responded_attimestamp.responded_at(no bump).{"response": "Skip", "response_note": "Got it"}.Observe 409 with detail
"Notification has already been responded to". GET the notification —response,response_note, andresponded_atare unchanged from step 2.{"response": "Done", "response_note": "different"}.Observe 409, row unchanged.
pytest tests/test_notification_respond.py -v— 27 tests pass.Deviations
D-10 —
test_409_already_respondedwas renamed and modified.The pre-existing test posted
{"response": "Done"}twice and asserted409. Under the new contract that case is the canonical idempotent
success path (200), so the test cannot survive unchanged. Renamed to
test_409_already_responded_with_different_responseand changed thesecond call to
{"response": "Skip"}so it still represents a 409conflict path under the new semantics. The spec's acceptance line
"Existing 409/410/404 tests continue to pass" is otherwise honored —
all the other 409 (
pending), 410 (expired), and 404 (not found) tests pass without change. Flagging because the spec'sacceptance language read as if no test changes were needed in the
existing block; in practice this one case is the exact behavior being
flipped, so a test edit was unavoidable. Net test count: +4 (three
acceptance cases + a no-note idempotency variant).
Test Results
Targeted run on the changed file:
Acceptance Checklist
respond_to_notificationreturns 200 + existing row when(response, response_note)match the recorded values; no DBwrite,
responded_atnot bumped.respond_to_notificationreturns 409 whenresponsediffersfrom the recorded value.
respond_to_notificationreturns 409 whenresponsematchesbut
response_notediffers.pending→ 409,expired→ 410,not found→ 404 unchanged.tests/test_notification_respond.pycovers the threeacceptance-criteria cases.
change documented in Deviations).