diff --git a/pyproject.toml b/pyproject.toml index 3dbcfab..96700c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,9 @@ dependencies = [ [dependency-groups] dev = [ + # `httpx` is a dependency of FastAPI's `TestClient` class. + # Docs: https://fastapi.tiangolo.com/tutorial/testing/#using-testclient + "httpx>=0.28.1", "pre-commit>=4.1.0", "pyright>=1.1.386", "pytest>=8.3.5", diff --git a/src/README.md b/src/README.md index a3be001..7ae4822 100644 --- a/src/README.md +++ b/src/README.md @@ -9,3 +9,4 @@ - `lib/`: Library of helper functions, constants, etc. - `README.md`: This document - `server.py`: The BERtron API +- `tests/`: Tests targeting things implemented in this directory diff --git a/src/models.py b/src/models.py index 0ea55df..4770658 100644 --- a/src/models.py +++ b/src/models.py @@ -1,10 +1,15 @@ -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from typing import Optional class HealthResponse(BaseModel): r"""A response containing system health information.""" + # Raise a `ValidationError` if extra parameters are passed in when instantiating this class. + # Note: This facilitates having our tests confirm API responses don't include extra fields. + # Docs: https://docs.pydantic.dev/latest/api/config/#pydantic.config.ConfigDict.extra + model_config = ConfigDict(extra="forbid") + web_server: bool = Field( ..., title="Web server health", @@ -20,6 +25,8 @@ class HealthResponse(BaseModel): class VersionResponse(BaseModel): r"""A response containing system version information.""" + model_config = ConfigDict(extra="forbid") + api: Optional[str] = Field( ..., title="API version", diff --git a/src/server.py b/src/server.py index a46054e..201d5e6 100644 --- a/src/server.py +++ b/src/server.py @@ -24,7 +24,7 @@ "[View source](https://github.com/ber-data/bertron/blob/main/src/server.py)\n\n" f"[BERtron schema](https://ber-data.github.io/bertron-schema/) version: `{get_package_version('bertron-schema')}`" ), - version=get_package_version("bertron"), + version=f"{get_package_version('bertron')}", ) diff --git a/src/tests/test_server.py b/src/tests/test_server.py new file mode 100644 index 0000000..1181047 --- /dev/null +++ b/src/tests/test_server.py @@ -0,0 +1,34 @@ +r""" +This file contains tests targeting `src/server.py`. + +You can learn about testing FastAPI apps here: +https://fastapi.tiangolo.com/tutorial/testing/ +""" + +import pytest +from fastapi.testclient import TestClient +from starlette import status + +from models import VersionResponse +from server import app + + +@pytest.fixture +def test_client(): + test_client = TestClient(app) + yield test_client + + +def test_root_endpoint_redirects_to_api_docs(test_client: TestClient): + response = test_client.get("/", follow_redirects=False) + assert response.status_code == status.HTTP_307_TEMPORARY_REDIRECT + assert response.headers["location"] == "/docs" + + +def test_version_endpoint_returns_version_response(test_client: TestClient): + response = test_client.get("/version") + assert response.status_code == status.HTTP_200_OK + # Note: This will raise a `ValidationError` if the response is not + # a valid `VersionResponse` (e.g. if it has extra fields or + # its fields' values are of an incompatible data type). + _ = VersionResponse(**response.json()) diff --git a/uv.lock b/uv.lock index b96765d..4a408ea 100644 --- a/uv.lock +++ b/uv.lock @@ -119,6 +119,7 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "httpx" }, { name = "pre-commit" }, { name = "pyright" }, { name = "pytest" }, @@ -138,6 +139,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "httpx", specifier = ">=0.28.1" }, { name = "pre-commit", specifier = ">=4.1.0" }, { name = "pyright", specifier = ">=1.1.386" }, { name = "pytest", specifier = ">=8.3.5" }, @@ -439,7 +441,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [