Repository navigation
Fix orders 401/403 conflation, minor rate-limit gaps, stale mockClient docs - #20
Merged
Merged
Conversation
…mit gaps
QA pass found orders.py conflating "no session" with "wrong role" on all
four routes -- e.g. create_order's `if not current_user.is_authenticated
or current_user.role != "customer": raise ForbiddenError(...)` raised
403 for both cases. Every other blueprint (portfolios.py's own
_authorize_customer_id is the reference pattern) correctly returns 401
for no session and 403 only once a session exists with the wrong role.
A frontend that keys "session expired -> re-login" off 401 wouldn't
trigger re-auth on any orders screen.
Split each of the four checks into two: UnauthenticatedError first (401)
when there's no session at all, ForbiddenError after (403) only once a
session exists but the role is wrong. list_orders/get_order already had
the checks in separate if-statements but raised the wrong class on the
first one; create_order/approve_order had them combined in one boolean
and needed splitting. Updated test_list_orders_with_no_session_is_rejected
(previously asserted the wrong 403) and added
test_get_order_with_no_session_is_rejected (untested for this exact case
before).
Two smaller gaps from the same pass:
- POST /auth/logout carried no rate limit, unlike every sibling auth
route. Added 60/min, matching /session's own limit -- logout has no
meaningful abuse surface, but consistency with the stated per-endpoint
throttling policy matters.
- RateLimitedError's bare `code="rate_limited"` is never what a
flask-limiter throttle actually puts on the wire (that path goes
through the generic HTTPException handler and emits
"too_many_requests" instead) -- the class itself is genuinely used
(chat.py's daily query cap, always with its own code= override), just
its default was flagged as misleading. Added a comment explaining the
two paths rather than removing anything, since deleting the class
would break chat.py's real usage.
Also: two frontend comments and one README bullet claimed the /lots,
/admin/customers*, /admin/kyc-overrides/<id>, and
/statements/<period>/export backend routes "don't exist yet" -- false;
all four are live, registered, and covered by the 799 backend tests.
The frontend genuinely hasn't been wired to them (still on mockClient,
honestly labelled "Simulated" in the UI per
docs/requirements/real-vs-simulated-rules.md -- that disclosure is
correct and unchanged), but the routes exist. Corrected all three
claims to say so.
Verified: full backend suite (uv run pytest) run twice -- 799 passed,
7 skipped, 2 xpassed both times, no flakes. ruff and mypy --strict clean
on every touched file (2 pre-existing, unrelated mypy errors remain
elsewhere, confirmed not introduced by this change). Live-verified the
401 fix against the running local backend: GET /api/v1/orders with no
cookie now returns 401 {"code":"unauthenticated"}. Frontend lint and
build clean (comment-only changes, no behavior touched).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Problem
An independent QA pass (extensive live API testing against the running local backend) found
orders.pyconflating "no session" with "wrong role" on all four routes:GET /api/v1/orderswith no cookie at all returned403 {"code":"forbidden"}instead of401. Every other blueprint gets this right —portfolios.py's_authorize_customer_idis the reference pattern already used correctly elsewhere in the codebase. A frontend that keys "session expired → re-login" off 401 would never trigger re-auth on any orders screen.Two smaller gaps surfaced by the same pass, unrelated but cheap to fix alongside:
POST /auth/logoutcarried no rate limit, unlike every sibling auth route.RateLimitedError's barecode="rate_limited"is never what a flask-limiter throttle actually puts on the wire (that path goes through the genericHTTPExceptionhandler and emitstoo_many_requestsinstead) — misleading to a future reader, though the class itself is genuinely used elsewhere (chat.py's daily query cap, always with its owncode=override).Also: two frontend comments and one README bullet claimed the
/lots,/admin/customers*,/admin/kyc-overrides/<id>, and/statements/<period>/exportbackend routes "don't exist yet" — false, all four are live, registered, and covered by the 799 backend tests. The frontend genuinely hasn't been wired to them yet (still onmockClient, honestly labelled "Simulated" in the UI perdocs/requirements/real-vs-simulated-rules.md— that disclosure is correct and unchanged), but the routes exist.Implementation
orders.pychecks into two:UnauthenticatedError(401) when there's no session,ForbiddenError(403) only once a session exists but the role is wrong.POST /auth/logoutgets@limiter.limit("60 per minute"), matching/session's own limit.RateLimitedErrorexplaining the two 429 paths, rather than removing anything (deleting the class would breakchat.py's real usage).Validation
uv run pytest tests/api/test_orders.py -q→ 18/18 passed, including an updated assertion (test_list_orders_with_no_session_is_rejectedpreviously asserted the wrong 403) and a new test (test_get_order_with_no_session_is_rejected, untested for this exact case before).ruff checkandmypy --strictclean on every touched file (2 pre-existing, unrelated mypy errors remain elsewhere — confirmed present before this change).GET /api/v1/orderswith no cookie now returns401 {"code":"unauthenticated"}.npm run lintandnpm run buildclean.Security / DB / API impact
401instead of403for a fully unauthenticated request. Any client correctly distinguishing 401 (re-authenticate) from 403 (forbidden) benefits; nothing currently in this codebase relied on the wrong code.🤖 Generated with Claude Code