Introduce an OpenAPI-first HTTP client driven by the same specification
used for server-side route binding, and refactor documentation and templates to treat the client as a first-class contract consumer. Key changes: - Add OpenAPI-first client module based on httpx - Document client usage alongside server-side binder usage - Update mkdocs navigation to include client documentation - Refactor CRUD and model app templates to call APIs via operationId instead of hardcoded paths - Align package documentation and public API surface with client support - Clarify server/client dependency split (fastapi vs httpx) This establishes strict symmetry between OpenAPI-driven server binding and OpenAPI-driven client invocation, reinforcing OpenAPI as the single source of truth on both sides of the contract.
This commit is contained in:
@@ -13,14 +13,22 @@ The tests assume:
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from main import app
|
||||
from openapi_first.loader import load_openapi
|
||||
from openapi_first.client import OpenAPIClient
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
spec = load_openapi("openapi.yaml")
|
||||
client = OpenAPIClient(
|
||||
spec=spec,
|
||||
base_url="http://testserver",
|
||||
client=client,
|
||||
)
|
||||
|
||||
|
||||
def test_list_items_initial():
|
||||
"""Initial items should be present."""
|
||||
response = client.get("/items")
|
||||
response = client.list_items()
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
@@ -34,7 +42,9 @@ def test_list_items_initial():
|
||||
|
||||
def test_get_item():
|
||||
"""Existing item should be retrievable by ID."""
|
||||
response = client.get("/items/1")
|
||||
response = client.get_item(
|
||||
path_params={"item_id": 1}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
item = response.json()
|
||||
@@ -50,7 +60,9 @@ def test_create_item():
|
||||
"price": 0.8,
|
||||
}
|
||||
|
||||
response = client.post("/items", json=payload)
|
||||
response = client.create_item(
|
||||
body=payload
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
item = response.json()
|
||||
@@ -59,7 +71,7 @@ def test_create_item():
|
||||
assert item["price"] == payload["price"]
|
||||
|
||||
# Verify it appears in list
|
||||
list_response = client.get("/items")
|
||||
list_response = client.list_items()
|
||||
ids = {i["id"] for i in list_response.json()}
|
||||
assert item["id"] in ids
|
||||
|
||||
@@ -71,7 +83,10 @@ def test_update_item():
|
||||
"price": 0.6,
|
||||
}
|
||||
|
||||
response = client.put("/items/1", json=payload)
|
||||
response = client.update_item(
|
||||
path_params={"item_id": 1},
|
||||
body=payload,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
item = response.json()
|
||||
@@ -80,7 +95,9 @@ def test_update_item():
|
||||
assert item["price"] == payload["price"]
|
||||
|
||||
# Verify persisted update
|
||||
get_response = client.get("/items/1")
|
||||
get_response = client.get_item(
|
||||
path_params={"item_id": 1}
|
||||
)
|
||||
updated = get_response.json()
|
||||
assert updated["name"] == payload["name"]
|
||||
assert updated["price"] == payload["price"]
|
||||
@@ -88,10 +105,12 @@ def test_update_item():
|
||||
|
||||
def test_delete_item():
|
||||
"""Deleting an item should remove it from the store."""
|
||||
response = client.delete("/items/2")
|
||||
response = client.delete_item(
|
||||
path_params={"item_id": 2}
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
# Verify deletion
|
||||
list_response = client.get("/items")
|
||||
list_response = client.list_items()
|
||||
ids = {item["id"] for item in list_response.json()}
|
||||
assert 2 not in ids
|
||||
|
||||
Reference in New Issue
Block a user