A Django + DRF poll/voting API demonstrating that "one vote per user per poll" only actually holds under concurrency when it is enforced by a database constraint, not an application-level check -- with a curated set of bugs and gaps layered on top of a real, tested authorization and results-aggregation surface.
Nobody has started this yet — be first.
Poll creators are locked out of their own polls; strangers can edit anyone's
Any authenticated user can currently PATCH (deactivate) any other user's poll -- a poll's creator has zero special standing except that they, specifically, are the one person forbidden from touching it. In practice: a user creates a poll, a completely unrelated user closes it out from under them, and the creator gets a 403 Forbidden if they try to fix it themselves. This is a genuine authorization bug (anyone can tamper with anyone else's poll) as well as a usability dead end (the actual owner can't manage their own content), and it's the kind of thing that generates angry support tickets ("why can't I edit my own poll?") right alongside abuse reports ("someone closed my poll and I don't know who").
About this project
A small but complete Django REST Framework API for creating polls, voting once per poll, and viewing live results: token-authenticated poll creation and voting, an is_active flag a creator can flip to close a poll, and a /results/ endpoint that recomputes vote counts and percentages from the database on every call rather than trusting any cached total. Celery + Redis dispatch a "thanks for voting" confirmation email asynchronously, off the request/response path, so the voting endpoint returns immediately rather than waiting on outbound mail.
The project exists to make one point concretely: "check, then write" is not a substitute for a real constraint. does perform a fast has-this-user-voted pre-check before writing -- a legitimate optimization, since it avoids a database round trip in the common case -- but the actual guarantee against a duplicate vote is 's , enforced inside with the resulting caught and turned into a clean 409. proves the distinction directly: it fires several concurrent calls with the pre-check mocked out entirely and asserts exactly one row survives. The same theme -- state that looks safe to trust in memory but isn't once a second request can interleave -- resurfaces in two of the seven practice tickets: a stale read that lets a vote land on a poll that was deactivated moments earlier, and a vote-change feature that has to update an existing row atomically rather than delete-then-recreate it, to avoid a window where a voter momentarily has zero or two votes.
A poll can be created with only one option
A poll exists to let people choose between at least two things. Right now POST /api/polls/ will happily create a poll with a single option -- there is nothing to vote between, so every vote on it is a foregone conclusion, and the "results" page will show 100% for the only choice no matter who votes. Beyond looking broken, this is exactly the kind of input a malformed client (a typo'd form, a partially-filled draft submitted early) can trigger by accident, silently creating junk polls that clutter every listing and confuse anyone who opens one expecting a real choice.
polls/services.cast_vote()VoteUniqueConstraint(fields=["user", "poll"])transaction.atomic()IntegrityErrorpolls/tests/test_race.pycast_vote()Voteis_activeSeven tickets are deliberately layered onto an otherwise working, fully-tested codebase: four injected bugs (an inverted permission check, a boundary condition on option count, a miscounted results total, and -- the deliberately hardest one -- a genuine, pre-existing stale-read race the audit for this ticket set turned up rather than injected), plus three missing enhancements (query-param filtering, a "what did I vote for" lookup, and a concurrency-safe vote-change endpoint). Every ticket has its own dedicated test module under practicetickets/, separate from the project's real polls/tests/ suite, and three of the seven also collaterally break existing tests in polls/tests/ that come back green again once the underlying bug is actually fixed.
DEBUGDB_*CELERY_BROKER_URLmanage.py test