Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import itertools

from flask import Flask, jsonify, request
from datetime import datetime, timezone

Expand Down Expand Up @@ -27,7 +29,7 @@ def create_item():
return jsonify(error="name is required"), 400

item = {
"id": len(_get_items()) + 1,
"id": _new_id(),
"name": data["name"],
"done": False,
}
Expand Down Expand Up @@ -63,6 +65,11 @@ def delete_item(item_id):


_items_store = []
_id_counter = itertools.count(1)


def _new_id():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] race-condition

The _new_id() function uses a non-atomic read-increment-return on a global counter, which is not thread-safe under threaded WSGI servers. However, this is a pre-existing concern — the original len(_get_items()) + 1 was equally non-thread-safe. This PR does not regress thread safety.

Suggested fix: Use itertools.count(1) with next() for an effectively atomic counter under CPython, or protect with threading.Lock if broader runtime support is needed.

return next(_id_counter)


def _get_items():
Expand All @@ -71,7 +78,9 @@ def _get_items():

def reset_items():
"""Reset the in-memory store (used by tests)."""
global _id_counter
_items_store.clear()
_id_counter = itertools.count(1)


if __name__ == "__main__":
Expand Down
45 changes: 45 additions & 0 deletions test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,48 @@ def test_delete_item(client):
def test_delete_item_not_found(client):
resp = client.delete("/items/999")
assert resp.status_code == 404


def test_no_id_collision_after_delete(client):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[low] test-adequacy

The new test validates the primary bug scenario (single delete then create) but does not cover multi-delete scenarios, which would provide stronger regression protection.

Suggested fix: Add a test that deletes multiple items then creates multiple new ones, verifying no ID collisions occur.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[info] test-docstring-convention

test_no_id_collision_after_delete() includes a docstring, but none of the existing 8 test functions use docstrings. Minor convention deviation.

"""IDs must never be reused after deletion."""
resp1 = client.post("/items", json={"name": "A"})
resp2 = client.post("/items", json={"name": "B"})
id_a = resp1.get_json()["id"]
id_b = resp2.get_json()["id"]

# Delete first item
client.delete(f"/items/{id_a}")

# Create a new item — should NOT collide with B's id
resp3 = client.post("/items", json={"name": "C"})
id_c = resp3.get_json()["id"]
assert id_c != id_b, "New item ID collides with existing item"

# Verify both remaining items exist in the list
all_items = client.get("/items").get_json()["items"]
all_ids = [item["id"] for item in all_items]
assert id_b in all_ids
assert id_c in all_ids
assert len(all_items) == 2


def test_no_id_reuse_after_multiple_deletes(client):
"""IDs stay unique after several deletions followed by several creates."""
# Create three items
ids = []
for name in ("X", "Y", "Z"):
resp = client.post("/items", json={"name": name})
ids.append(resp.get_json()["id"])

# Delete all three
for item_id in ids:
client.delete(f"/items/{item_id}")

# Create two more items — their IDs must not collide with any prior ID
new_ids = []
for name in ("P", "Q"):
resp = client.post("/items", json={"name": name})
new_ids.append(resp.get_json()["id"])

all_assigned = ids + new_ids
assert len(all_assigned) == len(set(all_assigned)), "ID reuse detected"
Loading