Skip to content

Commit

Permalink
move to TestBackend for tests
Browse files Browse the repository at this point in the history
  • Loading branch information
cwygoda authored and c-wygoda committed Apr 17, 2024
1 parent a7e6442 commit 3e87a4d
Show file tree
Hide file tree
Showing 21 changed files with 212 additions and 116 deletions.
8 changes: 0 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,6 @@ by `./scripts/bootstrap`.
A `pytest` based test suite is provided. Run it as `./scripts/test`. Any additional
pytest flags are passed along

A number of STAT specific pytest options are available through the test suite:

- `--stat-backend`: backend implementation to use in tests, defaults to
`stat_fastapi_mock_backend:StatMockBackend`
- `--stat-prefix`: service URL prefix, defaults to `/prefix`
- `--stat-product-id`: STAT product id to use in tests, defaults to
`mock:standard`

### Dev Server

For dev purposes, [stat_fastapi.**dev**.py](./stat_fastapi/__dev__.py) shows
Expand Down
41 changes: 12 additions & 29 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from importlib import import_module
from typing import Callable, Generator, TypeVar
from urllib.parse import urljoin

from fastapi import FastAPI
from fastapi.testclient import TestClient
from pytest import MonkeyPatch, Parser, fixture
from pytest import Parser, fixture

from stat_fastapi.api import StatApiRouter
from stat_fastapi_test_backend import TestBackend

T = TypeVar("T")

Expand All @@ -17,57 +17,40 @@ def pytest_addoption(parser: Parser):
parser.addoption(
"--stat-backend",
action="store",
default="stat_fastapi_mock_backend:StatMockBackend",
default="stat_fastapi_test_backend:TestBackend",
)
parser.addoption("--stat-prefix", action="store", default="/stat")
parser.addoption("--stat-product-id", action="store", default="mock:standard")


@fixture(scope="session")
def product_id(request) -> YieldFixture[str]:
yield request.config.getoption("--stat-product-id")


@fixture(scope="session")
def base_url() -> YieldFixture[str]:
yield "http://statserver"


@fixture(scope="session")
def router_prefix(request) -> YieldFixture[str]:
prefix = request.config.getoption("--stat-prefix").rstrip("/").strip()
prefix = prefix if prefix != "/" else ""
yield prefix
@fixture
def stat_backend():
yield TestBackend()


@fixture
def stat_client(
request, monkeypatch: MonkeyPatch, base_url: str, router_prefix: str
) -> YieldFixture[TestClient]:
def stat_client(stat_backend, base_url: str) -> YieldFixture[TestClient]:
app = FastAPI()

module, backend = request.config.getoption("--stat-backend").split(":", 1)
monkeypatch.setenv("DATABASE", "sqlite://")
module = import_module(module)
backend = getattr(module, backend)()

app.include_router(
StatApiRouter(backend=backend).router,
prefix=router_prefix,
StatApiRouter(backend=stat_backend).router,
prefix="",
)

yield TestClient(app, base_url=f"{base_url}{router_prefix}")
yield TestClient(app, base_url=f"{base_url}")


@fixture(scope="session")
def url_for(base_url: str, router_prefix: str) -> YieldFixture[Callable[[str], str]]:
def url_for(base_url: str) -> YieldFixture[Callable[[str], str]]:
def with_trailing_slash(value: str) -> str:
return value if value.endswith("/") else f"{value}/"

def url_for(value: str) -> str:
prefixed_path = urljoin(
with_trailing_slash(router_prefix), f"./{value.lstrip('/')}"
)
return urljoin(with_trailing_slash(base_url), f"./{prefixed_path.lstrip('/')}")
return urljoin(with_trailing_slash(base_url), f"./{value.lstrip('/')}")

yield url_for
2 changes: 1 addition & 1 deletion stat_fastapi/__dev__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
exit(1)

from stat_fastapi.api import StatApiRouter
from stat_fastapi_mock_backend import StatMockBackend
from stat_fastapi_tle_backend import StatMockBackend


class DevSettings(BaseSettings):
Expand Down
23 changes: 1 addition & 22 deletions stat_fastapi/models/order.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,17 @@
from enum import Enum
from typing import Literal

from geojson_pydantic import Feature
from geojson_pydantic.geometries import Geometry
from pydantic import AwareDatetime, ConfigDict

from stat_fastapi.models.constraints import Constraints
from stat_fastapi.models.shared import Link
from stat_fastapi.types.datetime_interval import DatetimeInterval


class OrderPayload(Feature[Geometry, Constraints]):
product_id: str


class OrderStatus(str, Enum):
pending = "pending"
processing = "processing"
finished = "finished"
failed = "failed"
expired = "expired"


class OrderProperties(Constraints):
datetime: DatetimeInterval

status: OrderStatus = OrderStatus.pending
created_at: AwareDatetime
updated_at: AwareDatetime

model_config = ConfigDict(extra="allow")


class Order(Feature[Geometry, OrderProperties]):
class Order(Feature[Geometry, Constraints]):
type: Literal["Feature"] = "Feature"
product_id: str
links: list[Link]
1 change: 1 addition & 0 deletions stat_fastapi/types/datetime_interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def serialize(
value: tuple[datetime, datetime],
serializer: Callable[[tuple[datetime, datetime]], tuple[str, str]],
) -> str:
return f"{value[0].isoformat()}/{value[1].isoformat()}"
serialized = serializer(value)
return f"{serialized[0]}/{serialized[1]}"

Expand Down
2 changes: 1 addition & 1 deletion stat_fastapi/types/datetime_interval_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def test_serialize(tz):
end = start + timedelta(hours=1)
model = Model(datetime=(start, end))

format = "%Y-%m-%d %H:%M:%S.%f%z"
format = "%Y-%m-%dT%H:%M:%S.%f%z"
expected = f"{rfc3339_strftime(start, format)}/{rfc3339_strftime(end, format)}"

obj = model.model_dump()
Expand Down
3 changes: 0 additions & 3 deletions stat_fastapi_mock_backend/__init__.py

This file was deleted.

3 changes: 3 additions & 0 deletions stat_fastapi_test_backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from stat_fastapi_test_backend.backend import TestBackend

__all__ = ["TestBackend"]
68 changes: 68 additions & 0 deletions stat_fastapi_test_backend/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from typing import Mapping
from uuid import uuid4

from fastapi import Request

from stat_fastapi.exceptions import ConstraintsException, NotFoundException
from stat_fastapi.models.opportunity import Opportunity, OpportunitySearch
from stat_fastapi.models.order import Order, OrderPayload
from stat_fastapi.models.product import Product


class TestBackend:
_products: list[Product] = []
_opportunities: list[Opportunity] = []
_allowed_order_payloads: list[OrderPayload] = []
_orders: Mapping[str, Order] = {}

def products(self, request: Request) -> list[Product]:
"""
Return a list of supported products.
"""
return self._products

def product(self, product_id: str, request: Request) -> Product | None:
"""
Return the product identified by `product_id` or `None` if it isn't
supported.
"""
try:
return next(
(product for product in self._products if product.id == product_id)
)
except StopIteration as exc:
raise NotFoundException() from exc

async def search_opportunities(
self, search: OpportunitySearch, request: Request
) -> list[Opportunity]:
return [
o.model_copy(update={"constraints": search.properties})
for o in self._opportunities
]

async def create_order(self, payload: OrderPayload, request: Request) -> Order:
"""
Create a new order.
"""
allowed = any(allowed == payload for allowed in self._allowed_order_payloads)
if allowed:
order = Order(
id=str(uuid4()),
geometry=payload.geometry,
properties=payload.properties,
product_id=payload.product_id,
links=[],
)
self._orders[order.id] = order
return order
raise ConstraintsException("not allowed")

async def get_order(self, order_id: str, request: Request):
"""
Show details for order with `order_id`.
"""
try:
return self._orders[order_id]
except KeyError:
raise NotFoundException()
3 changes: 3 additions & 0 deletions stat_fastapi_tle_backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from stat_fastapi_tle_backend.backend import StatMockBackend

__all__ = ["StatMockBackend"]
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
from stat_fastapi.models.opportunity import Opportunity, OpportunitySearch
from stat_fastapi.models.order import Order, OrderPayload
from stat_fastapi.models.product import Product, Provider, ProviderRole
from stat_fastapi_mock_backend.models import (
from stat_fastapi_tle_backend.models import (
ValidatedOpportunitySearch,
ValidatedOrderPayload,
)
from stat_fastapi_mock_backend.repository import Repository
from stat_fastapi_mock_backend.satellite import EarthObservationSatelliteModel
from stat_fastapi_mock_backend.settings import Settings
from stat_fastapi_tle_backend.repository import Repository
from stat_fastapi_tle_backend.satellite import EarthObservationSatelliteModel
from stat_fastapi_tle_backend.settings import Settings


class OffNadirRange(BaseModel):
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""
from datetime import UTC, datetime, timedelta
from typing import Generator
Expand Down Expand Up @@ -71,3 +72,4 @@ def test_get_order_properties(get_order_response: Response):
assert order["properties"]["datetime"] == START_END_INTERVAL
assert order["properties"]["status"] == "pending"
assert order["properties"]["off_nadir"] == {"minimum": 0, "maximum": 30}
"""
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
from sqlalchemy.orm import Session, declarative_base, sessionmaker
from sqlalchemy.pool import StaticPool

from stat_fastapi.models.order import Order, OrderProperties
from stat_fastapi.models.constraints import Constraints
from stat_fastapi.models.order import Order

from .models import OffNadirRange, ValidatedOrderPayload

Expand Down Expand Up @@ -58,7 +59,7 @@ def to_feature(self) -> Order:
# SQLite drops TZ, patching back with UTC ¯\_(ツ)_/¯
return Order(
geometry=to_shape(self.geom),
properties=OrderProperties(
properties=Constraints(
datetime=(
self.dt_start.replace(tzinfo=UTC),
self.dt_end.replace(tzinfo=UTC),
Expand Down
File renamed without changes.
File renamed without changes.
61 changes: 61 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from datetime import UTC, datetime

from geojson_pydantic import Point
from pydantic import BaseModel
from pytest import fixture

from stat_fastapi.models.constraints import Constraints
from stat_fastapi.models.opportunity import Opportunity
from stat_fastapi.models.order import OrderPayload
from stat_fastapi.models.product import Product, Provider, ProviderRole


@fixture
def products():
class Constraints(BaseModel):
pass

yield [
Product(
id="mock:standard",
description="Mock backend's standard product",
license="CC0-1.0",
providers=[
Provider(
name="ACME",
roles=[
ProviderRole.licensor,
ProviderRole.producer,
ProviderRole.processor,
ProviderRole.host,
],
url="http://acme.example.com",
)
],
constraints=Constraints,
links=[],
)
]


@fixture
def opportunities():
yield [
Opportunity(
geometry=Point(type="Point", coordinates=[13.4, 52.5]),
properties={},
constraints=Constraints(datetime=(datetime.now(UTC), datetime.now(UTC))),
)
]


@fixture
def allowed_order_payloads(products: list[Product]):
yield [
OrderPayload(
type="Feature",
geometry=Point(type="Point", coordinates=[13.4, 52.5]),
product_id=products[0].id,
properties=Constraints(datetime=(datetime.now(UTC), datetime.now(UTC))),
),
]
Loading

0 comments on commit 3e87a4d

Please sign in to comment.