From fba52ed8f87224cb10fc26ebdac94cfd4273e5b0 Mon Sep 17 00:00:00 2001 From: Jarrett Keifer Date: Thu, 31 Oct 2024 18:31:48 -0700 Subject: [PATCH] get tests passing --- src/stapi_fastapi/responses.py | 9 +++ src/stapi_fastapi/routers/product_router.py | 28 +++---- src/stapi_fastapi/routers/root_router.py | 18 +---- tests/backends.py | 19 +++-- tests/conftest.py | 68 ++++++++++------ tests/opportunity_test.py | 25 +++--- tests/order_test.py | 88 ++++++++++++--------- tests/utils.py | 7 +- 8 files changed, 147 insertions(+), 115 deletions(-) create mode 100644 src/stapi_fastapi/responses.py diff --git a/src/stapi_fastapi/responses.py b/src/stapi_fastapi/responses.py new file mode 100644 index 0000000..e8f6e1a --- /dev/null +++ b/src/stapi_fastapi/responses.py @@ -0,0 +1,9 @@ +from fastapi.responses import JSONResponse + +from stapi_fastapi.constants import TYPE_GEOJSON + + +class GeoJSONResponse(JSONResponse): + def __init__(self, *args, **kwargs) -> None: + kwargs["media_type"] = TYPE_GEOJSON + super().__init__(*args, **kwargs) diff --git a/src/stapi_fastapi/routers/product_router.py b/src/stapi_fastapi/routers/product_router.py index 3aab967..ac5deb1 100644 --- a/src/stapi_fastapi/routers/product_router.py +++ b/src/stapi_fastapi/routers/product_router.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING, Self -from fastapi import APIRouter, HTTPException, Request, status +from fastapi import APIRouter, HTTPException, Request, Response, status from stapi_fastapi.constants import TYPE_GEOJSON, TYPE_JSON from stapi_fastapi.exceptions import ConstraintsException @@ -13,6 +13,7 @@ from stapi_fastapi.models.order import Order from stapi_fastapi.models.product import Product from stapi_fastapi.models.shared import Link +from stapi_fastapi.responses import GeoJSONResponse from stapi_fastapi.types.json_schema_model import JsonSchemaModel if TYPE_CHECKING: @@ -44,13 +45,7 @@ def __init__( endpoint=self.search_opportunities, name=f"{self.root_router.name}:{self.product.id}:search-opportunities", methods=["POST"], - responses={ - 200: { - "content": { - "TYPE_GEOJSON": {}, - }, - } - }, + response_class=GeoJSONResponse, summary="Search Opportunities for the product", ) @@ -67,13 +62,8 @@ def __init__( endpoint=self.create_order, name=f"{self.root_router.name}:{self.product.id}:create-order", methods=["POST"], - responses={ - 201: { - "content": { - "TYPE_GEOJSON": {}, - }, - } - }, + response_class=GeoJSONResponse, + status_code=status.HTTP_201_CREATED, summary="Create an order for the product", ) @@ -104,6 +94,7 @@ async def search_opportunities( ) except ConstraintsException as exc: raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=exc.detail) + return OpportunityCollection(features=opportunities) async def get_product_constraints(self: Self) -> JsonSchemaModel: @@ -113,7 +104,7 @@ async def get_product_constraints(self: Self) -> JsonSchemaModel: return self.product.constraints async def create_order( - self, payload: OpportunityRequest, request: Request + self, payload: OpportunityRequest, request: Request, response: Response ) -> Order: """ Create a new order. @@ -127,6 +118,7 @@ async def create_order( except ConstraintsException as exc: raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, detail=exc.detail) - location = self.root_router.generate_order_href(request, order.id) - order.links.append(Link(href=str(location), rel="self", type=TYPE_GEOJSON)) + location = str(self.root_router.generate_order_href(request, order.id)) + order.links.append(Link(href=location, rel="self", type=TYPE_GEOJSON)) + response.headers["Location"] = location return order diff --git a/src/stapi_fastapi/routers/root_router.py b/src/stapi_fastapi/routers/root_router.py index b624cee..0b98836 100644 --- a/src/stapi_fastapi/routers/root_router.py +++ b/src/stapi_fastapi/routers/root_router.py @@ -10,6 +10,7 @@ from stapi_fastapi.models.product import Product, ProductsCollection from stapi_fastapi.models.root import RootResponse from stapi_fastapi.models.shared import Link +from stapi_fastapi.responses import GeoJSONResponse from stapi_fastapi.routers.product_router import ProductRouter @@ -56,13 +57,7 @@ def __init__( self.get_orders, methods=["GET"], name=f"{self.name}:list-orders", - responses={ - 200: { - "content": { - "TYPE_GEOJSON": {}, - }, - } - }, + response_class=GeoJSONResponse, tags=["Order"], ) @@ -71,13 +66,7 @@ def __init__( self.get_order, methods=["GET"], name=f"{self.name}:get-order", - responses={ - 200: { - "content": { - "TYPE_GEOJSON": {}, - }, - } - }, + response_class=GeoJSONResponse, tags=["Orders"], ) @@ -136,6 +125,7 @@ async def get_orders(self, request: Request) -> list[Order]: type=TYPE_JSON, ) ) + return orders async def get_order(self: Self, order_id: str, request: Request) -> Order: diff --git a/tests/backends.py b/tests/backends.py index 58067e9..6619b40 100644 --- a/tests/backends.py +++ b/tests/backends.py @@ -1,4 +1,3 @@ -from typing import Mapping from uuid import uuid4 from fastapi import Request @@ -9,8 +8,13 @@ from stapi_fastapi.models.product import Product -class TestRootBackend: - _orders: Mapping[str, Order] = {} +class MockOrderDB(dict[int | str, Order]): + pass + + +class MockRootBackend: + def __init__(self, orders: MockOrderDB) -> None: + self._orders = orders async def get_orders(self, request: Request) -> list[Order]: """ @@ -28,10 +32,11 @@ async def get_order(self, order_id: str, request: Request) -> Order: raise NotFoundException() -class TestProductBackend(ProductBackend): - _opportunities: list[Opportunity] = [] - _allowed_payloads: list[OpportunityRequest] = [] - _orders: Mapping[str, Order] = {} +class MockProductBackend(ProductBackend): + def __init__(self, orders: MockOrderDB) -> None: + self._opportunities: list[Opportunity] = [] + self._allowed_payloads: list[OpportunityRequest] = [] + self._orders = orders async def search_opportunities( self, product: Product, search: OpportunityRequest, request: Request diff --git a/tests/conftest.py b/tests/conftest.py index 0986778..c0a3aea 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,19 +12,42 @@ from stapi_fastapi.models.opportunity import ( Opportunity, OpportunityPropertiesBase, + OpportunityRequest, ) from stapi_fastapi.models.product import Product, Provider, ProviderRole from stapi_fastapi.routers.root_router import RootRouter -from .backend import TestProductBackend, TestRootBackend +from .backends import MockOrderDB, MockProductBackend, MockRootBackend class TestSpotlightProperties(OpportunityPropertiesBase): off_nadir: int +@pytest.fixture(scope="session") +def base_url() -> Iterator[str]: + yield "http://stapiserver" + + @pytest.fixture -def mock_product_test_spotlight(mock_provider_test: Provider) -> Product: +def order_db() -> MockOrderDB: + return MockOrderDB() + + +@pytest.fixture +def product_backend(order_db: MockOrderDB) -> MockProductBackend: + return MockProductBackend(order_db) + + +@pytest.fixture +def root_backend(order_db) -> MockRootBackend: + return MockRootBackend(order_db) + + +@pytest.fixture +def mock_product_test_spotlight( + product_backend: MockProductBackend, mock_provider: Provider +) -> Product: """Fixture for a mock Test Spotlight product.""" return Product( id="test-spotlight", @@ -32,28 +55,13 @@ def mock_product_test_spotlight(mock_provider_test: Provider) -> Product: description="Test product for test spotlight", license="CC-BY-4.0", keywords=["test", "satellite"], - providers=[mock_provider_test], + providers=[mock_provider], links=[], constraints=TestSpotlightProperties, - backend=TestProductBackend(), + backend=product_backend, ) -@pytest.fixture(scope="session") -def base_url() -> Iterator[str]: - yield "http://stapiserver" - - -@pytest.fixture -def product_backend() -> Iterator[TestProductBackend]: - yield TestProductBackend() - - -@pytest.fixture -def root_backend() -> Iterator[TestRootBackend]: - yield TestRootBackend() - - @pytest.fixture def stapi_client( root_backend, mock_product_test_spotlight, base_url: str @@ -63,7 +71,8 @@ def stapi_client( app = FastAPI() app.include_router(root_router, prefix="") - yield TestClient(app, base_url=f"{base_url}") + with TestClient(app, base_url=f"{base_url}") as client: + yield client @pytest.fixture(scope="session") @@ -83,8 +92,8 @@ def products(mock_product_test_spotlight) -> list[Product]: @pytest.fixture -def opportunities(products: list[Product]) -> Iterator[list[Opportunity]]: - yield [ +def opportunities(products: list[Product]) -> list[Opportunity]: + return [ Opportunity( geometry=Point(type="Point", coordinates=[13.4, 52.5]), properties={ @@ -97,7 +106,7 @@ def opportunities(products: list[Product]) -> Iterator[list[Opportunity]]: @pytest.fixture -def mock_provider_test() -> Provider: +def mock_provider() -> Provider: return Provider( name="Test Provider", description="A provider for Test data", @@ -128,3 +137,16 @@ def mock_test_spotlight_opportunities() -> List[Opportunity]: ), ), ] + + +@pytest.fixture +def allowed_payloads() -> list[OpportunityRequest]: + return [ + OpportunityRequest( + geometry=Point( + type="Point", coordinates=Position2D(longitude=13.4, latitude=52.5) + ), + datetime=(datetime.now(UTC), datetime.now(UTC)), + filter={}, + ), + ] diff --git a/tests/opportunity_test.py b/tests/opportunity_test.py index ce753d3..2e67d7f 100644 --- a/tests/opportunity_test.py +++ b/tests/opportunity_test.py @@ -1,12 +1,11 @@ -import json from datetime import UTC, datetime, timedelta from typing import List import pytest from fastapi.testclient import TestClient -from stapi_fastapi.models.opportunity import Opportunity +from stapi_fastapi.models.opportunity import Opportunity, OpportunityCollection -from .backend import TestProductBackend +from .backends import MockProductBackend from .datetime_interval_test import rfc3339_strftime @@ -14,9 +13,9 @@ def test_search_opportunities_response( product_id: str, mock_test_spotlight_opportunities: List[Opportunity], - product_backend: TestProductBackend, + product_backend: MockProductBackend, stapi_client: TestClient, -): +) -> None: product_backend._opportunities = mock_test_spotlight_opportunities now = datetime.now(UTC) @@ -48,15 +47,11 @@ def test_search_opportunities_response( # Use POST method to send the payload response = stapi_client.post(url, json=request_payload) - print(json.dumps(response.json(), indent=4)) - # Validate response status and structure assert response.status_code == 200, f"Failed for product: {product_id}" - assert isinstance( - response.json(), list - ), "Response should be a list of opportunities" - for opportunity in response.json(): - assert "id" in opportunity, "Opportunity item should have an 'id' field" - assert ( - "properties" in opportunity - ), "Opportunity item should have a 'properties' field" + _json = response.json() + + try: + OpportunityCollection(**_json) + except Exception as _: + pytest.fail("response is not an opportunity collection") diff --git a/tests/order_test.py b/tests/order_test.py index cb9b521..56849d6 100644 --- a/tests/order_test.py +++ b/tests/order_test.py @@ -1,55 +1,69 @@ from datetime import UTC, datetime, timedelta +import pytest +from fastapi import status +from fastapi.testclient import TestClient +from httpx import Response +from stapi_fastapi.models.opportunity import OpportunityRequest + +from .backends import MockProductBackend +from .utils import find_link + NOW = datetime.now(UTC) START = NOW END = START + timedelta(days=5) -# @fixture -# def new_order_response( -# stapi_backend: TestRootBackend, -# stapi_client: TestClient, -# allowed_payloads: list[OpportunityRequest], -# ) -> Generator[Response, None, None]: -# stapi_backend._allowed_payloads = allowed_payloads +@pytest.fixture +def new_order_response( + product_id: str, + product_backend: MockProductBackend, + stapi_client: TestClient, + allowed_payloads: list[OpportunityRequest], +) -> Response: + product_backend._allowed_payloads = allowed_payloads -# res = stapi_client.post( -# "/orders", -# json=allowed_payloads[0].model_dump(), -# ) + res = stapi_client.post( + f"products/{product_id}/order", + json=allowed_payloads[0].model_dump(), + ) -# assert res.status_code == status.HTTP_201_CREATED -# assert res.headers["Content-Type"] == "application/geo+json" -# yield res + assert res.status_code == status.HTTP_201_CREATED + assert res.headers["Content-Type"] == "application/geo+json" + return res -# def test_new_order_location_header_matches_self_link(new_order_response: Response): -# order = new_order_response.json() -# assert new_order_response.headers["Location"] == str( -# find_link(order["links"], "self")["href"] -# ) +@pytest.mark.parametrize("product_id", ["test-spotlight"]) +def test_new_order_location_header_matches_self_link( + new_order_response: Response, +) -> None: + order = new_order_response.json() + link = find_link(order["links"], "self") + assert link + assert new_order_response.headers["Location"] == str(link["href"]) -# @fixture -# def get_order_response( -# stapi_client: TestClient, new_order_response: Response -# ) -> Generator[Response, None, None]: -# order_id = new_order_response.json()["id"] +@pytest.fixture +def get_order_response( + stapi_client: TestClient, new_order_response: Response +) -> Response: + order_id = new_order_response.json()["id"] -# res = stapi_client.get(f"/orders/{order_id}") -# assert res.status_code == status.HTTP_200_OK -# assert res.headers["Content-Type"] == "application/geo+json" -# yield res + res = stapi_client.get(f"/orders/{order_id}") + assert res.status_code == status.HTTP_200_OK + assert res.headers["Content-Type"] == "application/geo+json" + return res -# def test_get_order_properties(get_order_response: Response, allowed_payloads): -# order = get_order_response.json() +@pytest.mark.parametrize("product_id", ["test-spotlight"]) +def test_get_order_properties(get_order_response: Response, allowed_payloads) -> None: + order = get_order_response.json() -# assert order["geometry"] == { -# "type": "Point", -# "coordinates": list(allowed_payloads[0].geometry.coordinates), -# } + assert order["geometry"] == { + "type": "Point", + "coordinates": list(allowed_payloads[0].geometry.coordinates), + } -# assert ( -# order["properties"]["datetime"] == allowed_payloads[0].model_dump()["datetime"] -# ) + assert ( + order["properties"]["datetime"] == allowed_payloads[0].model_dump()["datetime"] + ) diff --git a/tests/utils.py b/tests/utils.py index fdb3342..20947d4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,2 +1,7 @@ -def find_link(links: list[dict], rel: str) -> dict | None: +from typing import Any + +link_dict = dict[str, Any] + + +def find_link(links: list[link_dict], rel: str) -> link_dict | None: return next((link for link in links if link["rel"] == rel), None)