Skip to content

Commit 2f603fa

Browse files
committed
feat(api): 100% line coverage gate and PLAN §3.6
- Add pytest-cov with fail-under=100; omit generated OpenAPI models from coverage - Exclude Protocol ellipsis stubs from coverage report; pragma on codegen __main__ - Add codegen tests and repository edge-case tests for full measured coverage - Document testing and coverage in apps/api README; tick PLAN §3.6 - Ignore .coverage/htmlcov; clarify CI test step for coverage gate Made-with: Cursor
1 parent 78c7607 commit 2f603fa

8 files changed

Lines changed: 106 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
- name: Lint
3838
run: pnpm lint
3939

40-
- name: Test
40+
- name: Test (API 100% line coverage gate)
4141
run: pnpm test
4242

4343
- name: Generate OpenAPI code

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ venv/
1616
__pycache__/
1717
*.py[cod]
1818
.pytest_cache/
19+
.coverage
20+
.coverage.*
21+
htmlcov/
1922
.mypy_cache/
2023
.ruff_cache/
2124
*.egg-info/

PLAN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Tasks and subtasks for building the bread-recipes app (SolidJS + Python REST + O
2020
- [x] **3.3** Wire FastAPI/OpenAPI **`info`** (title, version, description) from **`packages/openapi/openapi.yaml`** so the running app matches the committed spec and those values are not duplicated in code (e.g. `main.py`).
2121
- [x] **3.4** Implement REST handlers to match the OpenAPI spec (response shapes and status codes); keep behaviour aligned with the spec.
2222
- [x] **3.5** Generate Pydantic models from **`packages/openapi/openapi.yaml`** (e.g. **datamodel-code-generator**), commit generated output, and add CI that fails when the spec changes without regenerating (drift check).
23-
- [ ] **3.6** Tests with 100% coverage and a coverage gate in CI for the API package; add `README.md` for install, run, and test commands.
23+
- [x] **3.6** Tests with 100% coverage and a coverage gate in CI for the API package; add `README.md` for install, run, and test commands.
2424
- [ ] **3.7** Select and configure a Python import-ordering tool (PEP 8–aligned; e.g. **Ruff**’s isort rules or **isort**), apply it across **`apps/api`**, and document how to run it (CI enforcement can align with §3.6 / §6.1).
2525

2626
## 4. SolidJS front end

apps/api/README.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Bread Recipes API (Python)
22

3-
FastAPI ASGI service: **`GET /health`**, **`GET /recipes`**, and **`GET /recipes/{recipe_id}`**; further PLAN work covers coverage and tooling.
3+
FastAPI ASGI service: **`GET /health`**, **`GET /recipes`**, and **`GET /recipes/{recipe_id}`**.
44

55
### Package layout
66

@@ -32,6 +32,22 @@ source .venv/bin/activate
3232
pip install -e ".[dev]"
3333
```
3434

35+
## Testing
36+
37+
From **`apps/api`** with dev dependencies installed (**`pip install -e ".[dev]"`**):
38+
39+
```bash
40+
pnpm test
41+
```
42+
43+
This runs **`pytest`** with **line coverage** for the **`app`** package, **fails under 100%**, and **omits** generated **`app/openapi/generated/`** (codegen output). To run tests without coverage (faster iteration):
44+
45+
```bash
46+
.venv/bin/python -m pytest --no-cov
47+
```
48+
49+
Configuration lives in **`pyproject.toml`** (**`[tool.pytest.ini_options]`**, **`[tool.coverage.*]`**).
50+
3551
## Run (development)
3652

3753
```bash
@@ -84,4 +100,4 @@ These editors use the **Python** extension (Cursor ships with equivalent behavio
84100

85101
If analysis still looks wrong, reload the window (**Developer: Reload Window** from the Command Palette) after changing the interpreter.
86102

87-
**Why this path:** analysis runs with `sys.path` set for that interpreter; the editable install of **`recipes-api`** is only guaranteed for the venv you created with **`pip install -e ".[dev]"`**.
103+
**Why this path:** analysis runs with `sys.path` set for that interpreter; the editable install of the **`recipes-api`** package (**`app`**) is only guaranteed for the venv you created with **`pip install -e ".[dev]"`**.

apps/api/app/openapi/codegen.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,5 @@ def main() -> None:
3232
)
3333

3434

35-
if __name__ == "__main__":
35+
if __name__ == "__main__": # pragma: no cover
3636
main()
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Tests for OpenAPI Pydantic codegen CLI."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
from unittest.mock import MagicMock
7+
8+
import pytest
9+
10+
from app.openapi import codegen
11+
12+
13+
def test_main_exits_when_openapi_file_missing(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
14+
missing = tmp_path / "missing.yaml"
15+
monkeypatch.setattr(
16+
"app.openapi.codegen.resolve_openapi_spec_path",
17+
lambda: missing,
18+
)
19+
with pytest.raises(SystemExit) as exc_info:
20+
codegen.main()
21+
assert exc_info.value.code == 1
22+
23+
24+
def test_main_invokes_datamodel_generate(
25+
monkeypatch: pytest.MonkeyPatch,
26+
tmp_path: Path,
27+
) -> None:
28+
spec = tmp_path / "openapi.yaml"
29+
spec.write_text(
30+
'openapi: "3.1.0"\n'
31+
"info:\n"
32+
' title: "T"\n'
33+
' version: "1"\n'
34+
' description: "D"\n'
35+
"paths: {}\n",
36+
encoding="utf-8",
37+
)
38+
out = tmp_path / "gen" / "openapi_models.py"
39+
monkeypatch.setattr("app.openapi.codegen.resolve_openapi_spec_path", lambda: spec)
40+
monkeypatch.setattr("app.openapi.codegen._OUTPUT", out)
41+
mock_generate = MagicMock()
42+
monkeypatch.setattr("app.openapi.codegen.generate", mock_generate)
43+
codegen.main()
44+
mock_generate.assert_called_once()
45+
assert out.parent.is_dir()

apps/api/app/routers/recipes/repository_test.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,29 @@ def test_invalid_file_raises(tmp_path: Path, bad_payload: str) -> None:
9090
StaticRecipeRepository(data_path=path)
9191

9292

93+
def test_array_entry_must_be_object(tmp_path: Path) -> None:
94+
path = tmp_path / "bad.json"
95+
path.write_text(json.dumps(["not-an-object"]), encoding="utf-8")
96+
with pytest.raises(ValueError, match=r"recipes\.json\[0\] must be an object"):
97+
StaticRecipeRepository(data_path=path)
98+
99+
100+
def test_ingredients_and_steps_must_be_arrays(tmp_path: Path) -> None:
101+
row: dict[str, Any] = {
102+
"id": "a",
103+
"title": "T",
104+
"summary": "S",
105+
"imageUrl": "https://example.com/1.jpg",
106+
"imageUrlLarge": "https://example.com/2.jpg",
107+
"ingredients": "not-a-list",
108+
"steps": ["mix"],
109+
}
110+
path = tmp_path / "bad.json"
111+
path.write_text(json.dumps([row]), encoding="utf-8")
112+
with pytest.raises(ValueError, match="ingredients and steps must be arrays"):
113+
StaticRecipeRepository(data_path=path)
114+
115+
93116
def test_duplicate_ids_raise(tmp_path: Path) -> None:
94117
recipes_with_duplicate_ids: list[dict[str, Any]] = [
95118
{

apps/api/pyproject.toml

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies = [
1919
dev = [
2020
"httpx==0.28.1",
2121
"pytest==8.3.5",
22+
"pytest-cov==6.0.0",
2223
]
2324

2425
[tool.hatch.build.targets.wheel]
@@ -29,4 +30,16 @@ exclude = ["**/app/**/*_test.py"]
2930
testpaths = ["app"]
3031
pythonpath = ["."]
3132
python_files = ["*_test.py"]
32-
addopts = "-q"
33+
addopts = "-q --cov=app --cov-report=term-missing --cov-fail-under=100"
34+
35+
[tool.coverage.run]
36+
source = ["app"]
37+
omit = ["app/openapi/generated/*"]
38+
39+
[tool.coverage.report]
40+
fail_under = 100
41+
exclude_lines = [
42+
"pragma: no cover",
43+
"if TYPE_CHECKING:",
44+
"^ *\\.\\.\\.$",
45+
]

0 commit comments

Comments
 (0)