Skip to content
This repository was archived by the owner on Apr 2, 2025. It is now read-only.

Commit f21a152

Browse files
committed
tests: add tests for async opportunity searching
1 parent 37bda4b commit f21a152

File tree

10 files changed

+550
-209
lines changed

10 files changed

+550
-209
lines changed

src/stapi_fastapi/backends/root_backend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"""
7272

7373
GetOpportunitySearchRecords = Callable[
74-
[Request, str | None, int],
74+
[str | None, int, Request],
7575
Coroutine[Any, Any, ResultE[tuple[list[OpportunitySearchRecord], Maybe[str]]]],
7676
]
7777
"""

src/stapi_fastapi/routers/product_router.py

Lines changed: 137 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -166,153 +166,182 @@ async def _create_order(
166166
)
167167

168168
def get_product(self: Self, request: Request) -> Product:
169-
return self.product.with_links(
170-
links=[
171-
Link(
172-
href=str(
173-
request.url_for(
174-
f"{self.root_router.name}:{self.product.id}:get-product",
175-
),
169+
links = [
170+
Link(
171+
href=str(
172+
request.url_for(
173+
f"{self.root_router.name}:{self.product.id}:get-product",
176174
),
177-
rel="self",
178-
type=TYPE_JSON,
179175
),
180-
Link(
181-
href=str(
182-
request.url_for(
183-
f"{self.root_router.name}:{self.product.id}:get-constraints",
184-
),
176+
rel="self",
177+
type=TYPE_JSON,
178+
),
179+
Link(
180+
href=str(
181+
request.url_for(
182+
f"{self.root_router.name}:{self.product.id}:get-constraints",
185183
),
186-
rel="constraints",
187-
type=TYPE_JSON,
188184
),
189-
Link(
190-
href=str(
191-
request.url_for(
192-
f"{self.root_router.name}:{self.product.id}:get-order-parameters",
193-
),
185+
rel="constraints",
186+
type=TYPE_JSON,
187+
),
188+
Link(
189+
href=str(
190+
request.url_for(
191+
f"{self.root_router.name}:{self.product.id}:get-order-parameters",
194192
),
195-
rel="order-parameters",
196-
type=TYPE_JSON,
197193
),
198-
Link(
199-
href=str(
200-
request.url_for(
201-
f"{self.root_router.name}:{self.product.id}:search-opportunities",
202-
),
194+
rel="order-parameters",
195+
type=TYPE_JSON,
196+
),
197+
Link(
198+
href=str(
199+
request.url_for(
200+
f"{self.root_router.name}:{self.product.id}:create-order",
203201
),
204-
rel="opportunities",
205-
type=TYPE_JSON,
206202
),
203+
rel="create-order",
204+
type=TYPE_JSON,
205+
),
206+
]
207+
208+
if (
209+
self.product.supports_opportunity_search
210+
or self.root_router.supports_async_opportunity_search
211+
):
212+
links.append(
207213
Link(
208214
href=str(
209215
request.url_for(
210-
f"{self.root_router.name}:{self.product.id}:create-order",
216+
f"{self.root_router.name}:{self.product.id}:search-opportunities",
211217
),
212218
),
213-
rel="create-order",
219+
rel="opportunities",
214220
type=TYPE_JSON,
215221
),
216-
],
217-
)
222+
)
218223

219-
async def search_opportunities( # noqa: C901
224+
return self.product.with_links(links=links)
225+
226+
async def search_opportunities(
220227
self: Self,
221228
search: OpportunityRequest,
222229
request: Request,
223230
response: Response,
224231
next: Annotated[str | None, Body()] = None,
225232
limit: Annotated[int, Body()] = 10,
226-
prefer: str | None = Depends(get_preference),
233+
prefer: Prefer | None = Depends(get_preference),
227234
) -> OpportunityCollection | Response:
228235
"""
229236
Explore the opportunities available for a particular set of constraints
230237
"""
231-
# synchronous opportunities search
232-
if (
233-
not self.root_router.supports_async_opportunity_search
234-
or prefer is Prefer.wait
238+
# sync
239+
if not self.root_router.supports_async_opportunity_search or (
240+
prefer is Prefer.wait and self.product.supports_opportunity_search
235241
):
236-
links: list[Link] = []
237-
match await self.product._search_opportunities(
238-
self,
242+
return await self.search_opportunities_sync(
239243
search,
244+
request,
245+
response,
246+
prefer,
240247
next,
241248
limit,
242-
request,
243-
):
244-
case Success((features, Some(pagination_token))):
245-
links.append(self.order_link(request))
246-
body = {
247-
"search": search.model_dump(mode="json"),
248-
"next": pagination_token,
249-
"limit": limit,
250-
}
251-
links.append(self.pagination_link(request, body))
252-
case Success((features, Nothing)): # noqa: F841
253-
links.append(self.order_link(request))
254-
case Failure(e) if isinstance(e, ConstraintsException):
255-
raise e
256-
case Failure(e):
257-
logger.error(
258-
"An error occurred while searching opportunities: %s",
259-
traceback.format_exception(e),
260-
)
261-
raise HTTPException(
262-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
263-
detail="Error searching opportunities",
264-
)
265-
case x:
266-
raise AssertionError(f"Expected code to be unreachable {x}")
267-
268-
if (
269-
prefer is Prefer.wait
270-
and self.root_router.supports_async_opportunity_search
271-
):
272-
response.headers["Preference-Applied"] = "wait"
273-
274-
return OpportunityCollection(features=features, links=links)
249+
)
275250

276-
# asynchronous opportunities search
251+
# async
277252
if (
278253
prefer is None
279254
or prefer is Prefer.respond_async
280255
or (prefer is Prefer.wait and not self.product.supports_opportunity_search)
281256
):
282-
match await self.product._search_opportunities_async(self, search, request):
283-
case Success(search_record):
284-
self.root_router.add_opportunity_search_record_self_link(
285-
search_record, request
286-
)
287-
headers = {}
288-
headers["Location"] = str(
289-
self.root_router.generate_opportunity_search_record_href(
290-
request, search_record.id
291-
)
292-
)
293-
if prefer is not None:
294-
headers["Preference-Applied"] = "respond-async"
295-
return JSONResponse(
296-
status_code=201,
297-
content=search_record.model_dump(mode="json"),
298-
headers=headers,
299-
)
300-
case Failure(e) if isinstance(e, ConstraintsException):
301-
raise e
302-
case Failure(e):
303-
logger.error(
304-
"An error occurred while initiating an asynchronous opportunity search: %s",
305-
traceback.format_exception(e),
306-
)
307-
raise HTTPException(
308-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
309-
detail="Error initiating an asynchronous opportunity search",
310-
)
311-
case y:
312-
raise AssertionError(f"Expected code to be unreachable: {y}")
257+
return await self.search_opportunities_async(search, request, prefer)
313258

314259
raise AssertionError("Expected code to be unreachable")
315260

261+
async def search_opportunities_sync(
262+
self: Self,
263+
search: OpportunityRequest,
264+
request: Request,
265+
response: Response,
266+
prefer: Prefer | None,
267+
next: Annotated[str | None, Body()] = None,
268+
limit: Annotated[int, Body()] = 10,
269+
) -> OpportunityCollection:
270+
links: list[Link] = []
271+
match await self.product._search_opportunities(
272+
self,
273+
search,
274+
next,
275+
limit,
276+
request,
277+
):
278+
case Success((features, Some(pagination_token))):
279+
links.append(self.order_link(request))
280+
body = {
281+
"search": search.model_dump(mode="json"),
282+
"next": pagination_token,
283+
"limit": limit,
284+
}
285+
links.append(self.pagination_link(request, body))
286+
case Success((features, Nothing)): # noqa: F841
287+
links.append(self.order_link(request))
288+
case Failure(e) if isinstance(e, ConstraintsException):
289+
raise e
290+
case Failure(e):
291+
logger.error(
292+
"An error occurred while searching opportunities: %s",
293+
traceback.format_exception(e),
294+
)
295+
raise HTTPException(
296+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
297+
detail="Error searching opportunities",
298+
)
299+
case x:
300+
raise AssertionError(f"Expected code to be unreachable {x}")
301+
302+
if prefer is Prefer.wait and self.root_router.supports_async_opportunity_search:
303+
response.headers["Preference-Applied"] = "wait"
304+
305+
return OpportunityCollection(features=features, links=links)
306+
307+
async def search_opportunities_async(
308+
self: Self,
309+
search: OpportunityRequest,
310+
request: Request,
311+
prefer: Prefer | None,
312+
) -> JSONResponse:
313+
match await self.product._search_opportunities_async(self, search, request):
314+
case Success(search_record):
315+
self.root_router.add_opportunity_search_record_self_link(
316+
search_record, request
317+
)
318+
headers = {}
319+
headers["Location"] = str(
320+
self.root_router.generate_opportunity_search_record_href(
321+
request, search_record.id
322+
)
323+
)
324+
if prefer is not None:
325+
headers["Preference-Applied"] = "respond-async"
326+
return JSONResponse(
327+
status_code=201,
328+
content=search_record.model_dump(mode="json"),
329+
headers=headers,
330+
)
331+
case Failure(e) if isinstance(e, ConstraintsException):
332+
raise e
333+
case Failure(e):
334+
logger.error(
335+
"An error occurred while initiating an asynchronous opportunity search: %s",
336+
traceback.format_exception(e),
337+
)
338+
raise HTTPException(
339+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
340+
detail="Error initiating an asynchronous opportunity search",
341+
)
342+
case x:
343+
raise AssertionError(f"Expected code to be unreachable: {x}")
344+
316345
def get_product_constraints(self: Self) -> JsonSchemaModel:
317346
"""
318347
Return supported constraints of a specific product

src/stapi_fastapi/routers/root_router.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ async def get_opportunity_search_records(
379379
self: Self, request: Request, next: str | None = None, limit: int = 10
380380
) -> OpportunitySearchRecords:
381381
links: list[Link] = []
382-
match await self._get_opportunity_search_records(request, next, limit):
382+
match await self._get_opportunity_search_records(next, limit, request):
383383
case Success((records, Some(pagination_token))):
384384
for record in records:
385385
self.add_opportunity_search_record_self_link(record, request)

tests/application.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,28 @@
88

99
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
1010

11-
from stapi_fastapi.models.conformance import CORE
11+
from stapi_fastapi.models.conformance import ASYNC_OPPORTUNITIES, CORE, OPPORTUNITIES
1212
from stapi_fastapi.routers.root_router import RootRouter
1313
from tests.backends import (
14+
mock_get_opportunity_search_record,
15+
mock_get_opportunity_search_records,
1416
mock_get_order,
1517
mock_get_order_statuses,
1618
mock_get_orders,
1719
)
18-
from tests.shared import InMemoryOrderDB, mock_product_test_spotlight
20+
from tests.shared import (
21+
InMemoryOpportunityDB,
22+
InMemoryOrderDB,
23+
product_test_spotlight_sync_async_opportunity,
24+
)
1925

2026

2127
@asynccontextmanager
2228
async def lifespan(app: FastAPI) -> AsyncIterator[dict[str, Any]]:
2329
try:
2430
yield {
2531
"_orders_db": InMemoryOrderDB(),
32+
"_opportunities_db": InMemoryOpportunityDB(),
2633
}
2734
finally:
2835
pass
@@ -32,8 +39,10 @@ async def lifespan(app: FastAPI) -> AsyncIterator[dict[str, Any]]:
3239
get_orders=mock_get_orders,
3340
get_order=mock_get_order,
3441
get_order_statuses=mock_get_order_statuses,
35-
conformances=[CORE],
42+
get_opportunity_search_records=mock_get_opportunity_search_records,
43+
get_opportunity_search_record=mock_get_opportunity_search_record,
44+
conformances=[CORE, OPPORTUNITIES, ASYNC_OPPORTUNITIES],
3645
)
37-
root_router.add_product(mock_product_test_spotlight)
46+
root_router.add_product(product_test_spotlight_sync_async_opportunity)
3847
app: FastAPI = FastAPI(lifespan=lifespan)
3948
app.include_router(root_router, prefix="")

0 commit comments

Comments
 (0)