Spaces:
Sleeping
Sleeping
Download tests/test_contract.py from pardeep-singh/reading-list: direct link, hf CLI and curl.
- Browser
- Download file 10.2 kB
-
https://huggingface.co/spaces/pardeep-singh/reading-list/resolve/main/tests/test_contract.py
- Command line
-
hf download hf://spaces/pardeep-singh/reading-list/tests/test_contract.py
-
curl -L -o test_contract.py https://huggingface.co/spaces/pardeep-singh/reading-list/resolve/main/tests/test_contract.py
10.2 kB
| """Black-box contract tests for docs/contract.md. | |
| HTTP only, through the `client` fixture (fresh app and empty store per test). | |
| No imports from app.store and no access to app.state. | |
| """ | |
| BOOKS = "/api/books" | |
| STATS = "/api/stats" | |
| STATUSES = ("to-read", "reading", "done") | |
| def add_book(client, **body): | |
| """POST a book and return the created Book JSON, asserting 201.""" | |
| response = client.post(BOOKS, json=body) | |
| assert response.status_code == 201, response.text | |
| return response.json() | |
| def patch_status(client, book_id, status): | |
| return client.patch(f"{BOOKS}/{book_id}", json={"status": status}) | |
| def assert_validation_error(response): | |
| """422 with FastAPI's default shape: detail is a list of items with loc and msg.""" | |
| assert response.status_code == 422, response.text | |
| detail = response.json()["detail"] | |
| assert isinstance(detail, list) | |
| assert len(detail) >= 1 | |
| for item in detail: | |
| assert "loc" in item | |
| assert "msg" in item | |
| # --- POST /api/books: success --------------------------------------------------- | |
| def test_create_returns_201_with_exact_book_shape(client): | |
| response = client.post( | |
| BOOKS, json={"title": "Dune", "author": "Frank Herbert", "status": "reading"} | |
| ) | |
| assert response.status_code == 201 | |
| assert response.json() == { | |
| "id": 1, | |
| "title": "Dune", | |
| "author": "Frank Herbert", | |
| "status": "reading", | |
| } | |
| def test_create_minimal_body_applies_defaults(client): | |
| response = client.post(BOOKS, json={"title": "Dune"}) | |
| assert response.status_code == 201 | |
| assert response.json() == { | |
| "id": 1, | |
| "title": "Dune", | |
| "author": None, | |
| "status": "to-read", | |
| } | |
| def test_create_assigns_incrementing_ids(client): | |
| ids = [add_book(client, title=f"Book {n}")["id"] for n in range(3)] | |
| assert ids == [1, 2, 3] | |
| def test_create_trims_title(client): | |
| assert add_book(client, title=" Dune ")["title"] == "Dune" | |
| def test_create_trims_author(client): | |
| assert add_book(client, title="Dune", author=" Frank Herbert ")["author"] == "Frank Herbert" | |
| def test_create_blank_author_becomes_null(client): | |
| assert add_book(client, title="Dune", author=" ")["author"] is None | |
| def test_create_empty_author_becomes_null(client): | |
| assert add_book(client, title="Dune", author="")["author"] is None | |
| def test_create_explicit_null_author_is_allowed(client): | |
| assert add_book(client, title="Dune", author=None)["author"] is None | |
| def test_create_accepts_each_status(client): | |
| for status in STATUSES: | |
| assert add_book(client, title="Dune", status=status)["status"] == status | |
| def test_create_accepts_200_character_title(client): | |
| title = "a" * 200 | |
| assert add_book(client, title=title)["title"] == title | |
| def test_create_title_length_is_checked_after_trimming(client): | |
| title = "a" * 200 | |
| assert add_book(client, title=f" {title} ")["title"] == title | |
| def test_create_accepts_200_character_author(client): | |
| author = "b" * 200 | |
| assert add_book(client, title="Dune", author=author)["author"] == author | |
| def test_create_allows_duplicate_titles(client): | |
| first = add_book(client, title="Dune") | |
| second = add_book(client, title="Dune") | |
| assert first["id"] != second["id"] | |
| def test_create_ignores_unknown_fields(client): | |
| response = client.post(BOOKS, json={"title": "Dune", "rating": 5}) | |
| assert response.status_code == 201 | |
| assert response.json() == {"id": 1, "title": "Dune", "author": None, "status": "to-read"} | |
| # --- POST /api/books: validation ------------------------------------------------ | |
| def test_create_rejects_missing_title(client): | |
| assert_validation_error(client.post(BOOKS, json={"author": "Frank Herbert"})) | |
| def test_create_rejects_empty_title(client): | |
| assert_validation_error(client.post(BOOKS, json={"title": ""})) | |
| def test_create_rejects_whitespace_only_title(client): | |
| assert_validation_error(client.post(BOOKS, json={"title": " "})) | |
| def test_create_rejects_201_character_title(client): | |
| assert_validation_error(client.post(BOOKS, json={"title": "a" * 201})) | |
| def test_create_rejects_non_string_title(client): | |
| assert_validation_error(client.post(BOOKS, json={"title": 123})) | |
| def test_create_rejects_null_title(client): | |
| assert_validation_error(client.post(BOOKS, json={"title": None})) | |
| def test_create_rejects_201_character_author(client): | |
| assert_validation_error(client.post(BOOKS, json={"title": "Dune", "author": "b" * 201})) | |
| def test_create_rejects_invalid_status(client): | |
| assert_validation_error(client.post(BOOKS, json={"title": "Dune", "status": "finished"})) | |
| def test_create_rejects_malformed_json(client): | |
| response = client.post( | |
| BOOKS, content=b'{"title": "Dune"', headers={"Content-Type": "application/json"} | |
| ) | |
| assert_validation_error(response) | |
| def test_rejected_create_does_not_store_a_book(client): | |
| client.post(BOOKS, json={"title": ""}) | |
| response = client.get(BOOKS) | |
| assert response.status_code == 200 | |
| assert response.json() == [] | |
| # --- GET /api/books ------------------------------------------------------------- | |
| def test_list_is_empty_array_when_store_is_empty(client): | |
| response = client.get(BOOKS) | |
| assert response.status_code == 200 | |
| assert response.json() == [] | |
| def test_list_returns_books_in_insertion_order(client): | |
| created = [ | |
| add_book(client, title="Dune", author="Frank Herbert", status="reading"), | |
| add_book(client, title="Emma", status="done"), | |
| add_book(client, title="Ulysses"), | |
| ] | |
| response = client.get(BOOKS) | |
| assert response.status_code == 200 | |
| assert response.json() == created | |
| def test_list_filters_by_each_status(client): | |
| by_status = { | |
| status: add_book(client, title=f"Book {status}", status=status) for status in STATUSES | |
| } | |
| for status, book in by_status.items(): | |
| response = client.get(BOOKS, params={"status": status}) | |
| assert response.status_code == 200 | |
| assert response.json() == [book] | |
| def test_list_filter_keeps_insertion_order(client): | |
| first = add_book(client, title="A", status="done") | |
| add_book(client, title="B", status="reading") | |
| third = add_book(client, title="C", status="done") | |
| response = client.get(BOOKS, params={"status": "done"}) | |
| assert response.json() == [first, third] | |
| def test_list_filter_with_no_matches_is_empty_array(client): | |
| add_book(client, title="Dune", status="to-read") | |
| response = client.get(BOOKS, params={"status": "done"}) | |
| assert response.status_code == 200 | |
| assert response.json() == [] | |
| def test_list_rejects_invalid_status_filter(client): | |
| assert_validation_error(client.get(BOOKS, params={"status": "finished"})) | |
| # --- PATCH /api/books/{id} ------------------------------------------------------ | |
| def test_patch_returns_updated_book(client): | |
| add_book(client, title="Dune", author="Frank Herbert", status="reading") | |
| response = patch_status(client, 1, "done") | |
| assert response.status_code == 200 | |
| assert response.json() == { | |
| "id": 1, | |
| "title": "Dune", | |
| "author": "Frank Herbert", | |
| "status": "done", | |
| } | |
| def test_patch_change_persists_in_list(client): | |
| add_book(client, title="Dune") | |
| patch_status(client, 1, "reading") | |
| assert client.get(BOOKS).json()[0]["status"] == "reading" | |
| def test_patch_only_changes_the_target_book(client): | |
| add_book(client, title="Dune") | |
| other = add_book(client, title="Emma") | |
| patch_status(client, 1, "done") | |
| assert client.get(BOOKS).json()[1] == other | |
| def test_patch_unknown_id_returns_404_book_not_found(client): | |
| response = patch_status(client, 999, "done") | |
| assert response.status_code == 404 | |
| assert response.json() == {"detail": "Book not found"} | |
| def test_patch_unknown_id_returns_404_when_other_books_exist(client): | |
| add_book(client, title="Dune") | |
| response = patch_status(client, 2, "done") | |
| assert response.status_code == 404 | |
| assert response.json() == {"detail": "Book not found"} | |
| def test_patch_rejects_invalid_status(client): | |
| add_book(client, title="Dune") | |
| assert_validation_error(patch_status(client, 1, "finished")) | |
| def test_patch_rejects_missing_status(client): | |
| add_book(client, title="Dune") | |
| assert_validation_error(client.patch(f"{BOOKS}/1", json={})) | |
| def test_patch_rejects_non_integer_id(client): | |
| assert_validation_error(patch_status(client, "abc", "done")) | |
| def test_rejected_patch_leaves_book_unchanged(client): | |
| book = add_book(client, title="Dune", status="reading") | |
| patch_status(client, 1, "finished") | |
| assert client.get(BOOKS).json() == [book] | |
| # --- GET /api/stats ------------------------------------------------------------- | |
| def test_stats_all_zero_when_empty(client): | |
| response = client.get(STATS) | |
| assert response.status_code == 200 | |
| assert response.json() == { | |
| "total": 0, | |
| "by_status": {"to-read": 0, "reading": 0, "done": 0}, | |
| } | |
| def test_stats_counts_after_adds(client): | |
| add_book(client, title="A", status="done") | |
| add_book(client, title="B", status="done") | |
| add_book(client, title="C") | |
| response = client.get(STATS) | |
| assert response.status_code == 200 | |
| assert response.json() == { | |
| "total": 3, | |
| "by_status": {"to-read": 1, "reading": 0, "done": 2}, | |
| } | |
| def test_stats_counts_after_patch(client): | |
| add_book(client, title="A") | |
| add_book(client, title="B") | |
| patch_status(client, 2, "reading") | |
| assert client.get(STATS).json() == { | |
| "total": 2, | |
| "by_status": {"to-read": 1, "reading": 1, "done": 0}, | |
| } | |
| def test_stats_total_equals_sum_of_by_status(client): | |
| for n, status in enumerate(STATUSES * 2): | |
| add_book(client, title=f"Book {n}", status=status) | |
| stats = client.get(STATS).json() | |
| assert set(stats["by_status"]) == set(STATUSES) | |
| assert stats["total"] == sum(stats["by_status"].values()) == 6 | |
| # --- GET / ---------------------------------------------------------------------- | |
| def test_index_serves_html(client): | |
| response = client.get("/") | |
| assert response.status_code == 200 | |
| assert response.headers["content-type"].startswith("text/html") | |