data/sample-service/ — a small FastAPI order-lookup service with two endpoints, five tests, and one bug placed on purpose. Every number below traces to that code and to a real test run, not to the README.
| Module | File | Owns |
|---|---|---|
| Service | app/main.py |
The FastAPI app, the in-memory ORDERS store (3 hardcoded orders), the order_total() pricing function, and both HTTP routes. |
| Tests | tests/test_orders.py |
Eight tests over the two routes and the pricing function, using FastAPI's TestClient. |
| Route | Input | Output |
|---|---|---|
GET /orders/{order_id} |
order_id — path string, e.g. A-1001 |
200: order object plus computed total.404 {"detail": "order not found"} when the id is not in the store. |
GET /accounts/{account}/total |
account — path string, matched exactly against every order's account field |
200: {"account", "orders" (count), "total"} summed across that account's orders.404 {"detail": "account not found"} when no order matches. |
tests/test_orders.py::test_total_two_lines — now passing, since the High-risk fix was applied to app/main.pyBefore the fix: assert 1256.0 == 1650.0 (FAILED) After the fix: assert 1650.0 == 1650.0 (PASSED)
app/main.py:17
order_total() was adding qty + unit instead of multiplying them, for every line in every order. It's why test_total_two_lines failed: a two-line order priced at 10 × 45.00 + 1 × 1200.00 = 1650.00 came back as 1256.00. Both endpoints returned this wrong number. The one case that looked fine by accident was the empty order in the sample data (B-2001), because summing nothing is 0 either way — that's why test_empty_order_total_is_zero passed even before the fix.
Fix applied — confirmed by re-running the suite (5 passed, 0 failed at the time; 11 passed, 0 failed now that the Medium and Low risks below are also fixed).
--- a/app/main.py +++ b/app/main.py @@ -14,7 +14,7 @@ def order_total(order: dict) -> float: total = 0.0 for line in order["lines"]: - total += line["qty"] + line["unit"] + total += line["qty"] * line["unit"] return round(total, 2)
app/main.py:16-18 (now 16-27 after the fix)
order_total() read line["qty"] and line["unit"] with no check that either key existed or held a number. A line missing one of those keys, or holding a string, raised an unhandled KeyError or TypeError that FastAPI turned into a bare 500, not a 4xx a caller could act on. There was no test for it because every line in the hardcoded store was well-formed; it only showed up once real or external data fed this store.
Fix applied — order_total() now validates each line and raises ValueError; both routes catch it and return 422 with a message naming the order and the bad line. Confirmed by three new tests: a missing key, a non-numeric value, and the live endpoint returning 422 for a malformed order.
--- a/app/main.py +++ b/app/main.py @@ -13,8 +13,16 @@ def order_total(order: dict) -> float: - """Sum of qty * unit across lines. Empty orders total 0.""" + """Sum of qty * unit across lines. Empty orders total 0. + + Raises ValueError if a line is missing qty/unit or holds a non-numeric + value, so callers can turn that into a clean 4xx instead of a bare 500. + """ total = 0.0 - for line in order["lines"]: - total += line["qty"] * line["unit"] + for i, line in enumerate(order["lines"]): + if "qty" not in line or "unit" not in line: + raise ValueError(f"order {order.get('id', '?')} line {i} is missing 'qty' or 'unit'") + qty, unit = line["qty"], line["unit"] + if not isinstance(qty, (int, float)) or isinstance(qty, bool) or \ + not isinstance(unit, (int, float)) or isinstance(unit, bool): + raise ValueError(f"order {order.get('id', '?')} line {i} has a non-numeric qty or unit") + total += qty * unit return round(total, 2) @@ get_order() and account_total() @@ ... - return {**order, "total": order_total(order)} + try: + total = order_total(order) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) + return {**order, "total": total}
app/main.py:31 (now the _normalize_account helper plus the route, around line 43-59)
account_total() compared o["account"] == account with no case-folding or trimming. "account a" or "Account A " (trailing space) returned a 404 for an account that existed. Low severity, since the caller controlled the exact string, but the kind of thing that becomes a support ticket the moment a second team writes the client.
Fix applied — both sides of the comparison are normalized with .strip().casefold() before matching, and the response returns the account name as stored (not whatever casing the caller sent), so the output stays consistent regardless of input. Confirmed by three new tests: lowercase input, a trailing space, and a genuine mismatch still returning 404. All eleven tests pass.
--- a/app/main.py +++ b/app/main.py @@ -43,7 +43,15 @@ +def _normalize_account(name: str) -> str: + """Case- and whitespace-insensitive key for account matching.""" + return name.strip().casefold() + + @app.get("/accounts/{account}/total") def account_total(account: str): - orders = [o for o in ORDERS.values() if o["account"] == account] + target = _normalize_account(account) + orders = [o for o in ORDERS.values() if _normalize_account(o["account"]) == target] if not orders: raise HTTPException(status_code=404, detail="account not found") ... - return {"account": account, "orders": len(orders), "total": total} + canonical = orders[0]["account"] + return {"account": canonical, "orders": len(orders), "total": total}
Add order lines below. The left total shows what the old code would have returned (qty + unit, the bug); the right total is what app/main.py actually returns today (qty × unit, fixed and confirmed by the test run above). This is the one interaction on this page — everything else is read-only.
| # | Qty | Unit price |
|---|
/codex:review is not reachable from this environment (no codex tool/connector present here), so this page and each fix were instead checked by an independent review pass before publishing. That pass flagged one real issue in this calculator's own wording, which was corrected. All three risks — High (wrong totals), Medium (unhandled malformed line), and Low (brittle account match) — have now been applied to app/main.py and confirmed by re-running the test suite: 11 passed, 0 failed.