73 lines of Python, two read-only endpoints, five tests, and a green suite that
proves almost nothing about the money. The deliberate bug this repo shipped with is
already fixed. The hole that let it through is not.
1
of 5 tests can catch a wrong total
Only test_total_two_lines asserts a computed amount that changes when the
arithmetic breaks. test_empty_order_total_is_zero returns 0.0 either way, and
neither endpoint test looks at total at all.
Tests passed
5
pytest -q, 0.34s
Tests failed
0
README says 1 should fail
Endpoints
2
both GET, both read-only
Open risks
3
1 critical, 1 serious, 1 warning
Lines of code
73
7 files, excluding .venv
The README is wrong, and that matters. It says “One test fails. That is the
bug.” All five pass. The bug — + where * belonged in
order_total — was fixed in commit 2c86b83, authored
7 Sep 2026 10:48. Nothing on this page claims credit for finding it. The risks below are what
is still in the code.
showing all
Nothing matches that.
Modules — what each one owns
Seven files carry all the behaviour. There is no layering: the data store, the money
arithmetic, and the HTTP routes all live in one 34-line module, so any change to one
touches the others.
Line counts from wc -l; .venv/, .git/ and caches excluded.
File
Lines
Owns
app/main.py
34
The ORDERS dict, order_total(), and both routes. Store, logic and transport in one file.
tests/test_orders.py
28
All 5 tests. Three go through TestClient; two call order_total() directly.
README.md
5
Run instructions. Lists no endpoints, and its test claim is out of date.
fastapi, httpx, pytest. No versions pinned, and no uvicorn — nothing here can serve the app.
app/__init__.py
0
Package marker. Empty.
tests/__init__.py
0
Package marker. Empty.
Endpoints — read off the source, not the README
The README documents no endpoints. Both of these were read from
app/main.py. Each carries a third, undocumented outcome the route
signature does not admit to.
GET/orders/{order_id}app/main.py:21
Input
order_id — path param, plain str. No pattern, no length bound, no validation. Looked up in a dict, so an odd value is a miss, not an injection.
Returns
200{id, account, lines[], total} — the stored order spread, plus a computed total as a float.
404{"detail": "order not found"}
500Undocumented.KeyError if the order has no lines, or a line has no qty/unit. See risk 2.
account — path param, plain str, matched with ==. Case- and whitespace-sensitive: "account a" and "Account A " both 404.
Returns
200{account, orders, total} — a count and a float sum over every matching order.
404{"detail": "account not found"} when nothing matches.
500Undocumented. Same KeyError path — it calls order_total() on every order it finds.
Cost
Full scan of ORDERS on every request. Fine at 3 orders; linear forever after.
Tested by
test_account_total — asserts status and orders == 2. The total this endpoint exists to return is never asserted.
Test coverage — 5 passed, 0 failed
Green, and thin. Each cell below is one test against one thing it could have checked.
Hover or focus a cell for what the test actually asserts. The columns are where the
risks live.
✓ Asserted — a wrong value fails the test~ Weak — asserted, but passes when the code is wrong– Gap — nothing asserted· Not applicable to this test
Read the money column. Four tests had a chance to assert a computed amount.
One does. One passes whatever the arithmetic says. Two ignore it. That is precisely how a
+-for-* bug lives in a repo with a green suite — and I checked
it: replaying the old buggy order_total against
test_empty_order_total_is_zero still returns 0.0, so that test
would have passed alongside the bug.
The three risks, ranked
Ranked by what a reader loses, not by how easy each is to fix. Every one was reproduced
against this code in this session — open a risk for the command and its real output.
Proposed fix for risk 1
Hold money as Decimal, round exactly once at the response boundary, and round
half-up the way invoices do. order_total() stays a float-returning function so
the two existing tests that import it keep working unchanged.
--- a/app/main.py+++ b/app/main.py@@ -1,5 +1,7 @@ """Order lookup service: two endpoints over an in-memory store."""+from decimal import Decimal, ROUND_HALF_UP+ from fastapi import FastAPI, HTTPException app = FastAPI(title="sample-service")@@ -13,10 +15,23 @@-def order_total(order: dict) -> float:- """Sum of qty * unit across lines. Empty orders total 0."""- total = 0.0- for line in order["lines"]:- total += line["qty"] * line["unit"]- return round(total, 2)+def order_subtotal(order: dict) -> Decimal:+ """Exact, unrounded sum of qty * unit across lines. Empty orders total 0."""+ total = Decimal("0")+ for line in order.get("lines") or []:+ total += Decimal(str(line.get("qty", 0))) * Decimal(str(line.get("unit", 0)))+ return total+++def as_money(amount: Decimal) -> float:+ """Round once, half-up, at the response boundary."""+ return float(amount.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))+++def order_total(order: dict) -> float:+ """Back-compatible float total for a single order."""+ return as_money(order_subtotal(order))@@ -29,6 +44,7 @@ @app.get("/accounts/{account}/total") def account_total(account: str): orders = [o for o in ORDERS.values() if o["account"] == account] if not orders: raise HTTPException(status_code=404, detail="account not found")- return {"account": account, "orders": len(orders), "total": round(sum(order_total(o) for o in orders), 2)}+ exact = sum((order_subtotal(o) for o in orders), Decimal("0"))+ return {"account": account, "orders": len(orders), "total": as_money(exact)}
Verified, not asserted. Applied to a scratch copy and run:
5 passed — the existing suite is unbroken. The failing case from
risk 1 goes from 0.06 to 0.09, which is
correct. As a side effect the KeyError crashes from risk 2 become
200s.
This diff does not close risk 2, and the side effect is not the fix. Missing
fields now read as 0, so a malformed order returns a confident, wrong
0.00 instead of crashing. Silent zero is not better than a 500. Risk 2 needs a
Pydantic model on the order shape returning 422, which is a larger change than
this one and deliberately out of scope here.