diff --git a/CHANGELOG.md b/CHANGELOG.md index cc3aa19f..e572c99d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Changed - Optimize data_loader.py script [#395](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/395) +- Refactored test configuration to use shared app config pattern [#399](https://github.com/stac-utils/stac-fastapi-elasticsearch-opensearch/pull/399) ### Removed diff --git a/examples/auth/README.md b/examples/auth/README.md index 0bd068e2..d1e4d85e 100644 --- a/examples/auth/README.md +++ b/examples/auth/README.md @@ -123,7 +123,7 @@ limited permissions to specific read-only endpoints. {"path": "/collections/{collection_id}", "method": ["GET"]}, {"path": "/collections/{collection_id}/items", "method": ["GET"]}, {"path": "/queryables", "method": ["GET"]}, - {"path": "/queryables/collections/{collection_id}/queryables", "method": ["GET"]}, + {"path": "/collections/{collection_id}/queryables", "method": ["GET"]}, {"path": "/_mgmt/ping", "method": ["GET"]} ], "dependencies": [ diff --git a/examples/auth/compose.basic_auth.yml b/examples/auth/compose.basic_auth.yml index ecfd555d..a5a7f4f6 100644 --- a/examples/auth/compose.basic_auth.yml +++ b/examples/auth/compose.basic_auth.yml @@ -21,7 +21,7 @@ services: - ES_USE_SSL=false - ES_VERIFY_CERTS=false - BACKEND=elasticsearch - - STAC_FASTAPI_ROUTE_DEPENDENCIES=[{"routes":[{"method":"*","path":"*"}],"dependencies":[{"method":"stac_fastapi.core.basic_auth.BasicAuth","kwargs":{"credentials":[{"username":"admin","password":"admin"}]}}]},{"routes":[{"path":"/","method":["GET"]},{"path":"/conformance","method":["GET"]},{"path":"/collections/{collection_id}/items/{item_id}","method":["GET"]},{"path":"/search","method":["GET","POST"]},{"path":"/collections","method":["GET"]},{"path":"/collections/{collection_id}","method":["GET"]},{"path":"/collections/{collection_id}/items","method":["GET"]},{"path":"/queryables","method":["GET"]},{"path":"/queryables/collections/{collection_id}/queryables","method":["GET"]},{"path":"/_mgmt/ping","method":["GET"]}],"dependencies":[{"method":"stac_fastapi.core.basic_auth.BasicAuth","kwargs":{"credentials":[{"username":"reader","password":"reader"}]}}]}] + - STAC_FASTAPI_ROUTE_DEPENDENCIES=[{"routes":[{"method":"*","path":"*"}],"dependencies":[{"method":"stac_fastapi.core.basic_auth.BasicAuth","kwargs":{"credentials":[{"username":"admin","password":"admin"}]}}]},{"routes":[{"path":"/","method":["GET"]},{"path":"/conformance","method":["GET"]},{"path":"/collections/{collection_id}/items/{item_id}","method":["GET"]},{"path":"/search","method":["GET","POST"]},{"path":"/collections","method":["GET"]},{"path":"/collections/{collection_id}","method":["GET"]},{"path":"/collections/{collection_id}/items","method":["GET"]},{"path":"/queryables","method":["GET"]},{"path":"/collections/{collection_id}/queryables","method":["GET"]},{"path":"/_mgmt/ping","method":["GET"]}],"dependencies":[{"method":"stac_fastapi.core.basic_auth.BasicAuth","kwargs":{"credentials":[{"username":"reader","password":"reader"}]}}]}] ports: - "8080:8080" volumes: @@ -55,7 +55,7 @@ services: - ES_USE_SSL=false - ES_VERIFY_CERTS=false - BACKEND=opensearch - - STAC_FASTAPI_ROUTE_DEPENDENCIES=[{"routes":[{"method":"*","path":"*"}],"dependencies":[{"method":"stac_fastapi.core.basic_auth.BasicAuth","kwargs":{"credentials":[{"username":"admin","password":"admin"}]}}]},{"routes":[{"path":"/","method":["GET"]},{"path":"/conformance","method":["GET"]},{"path":"/collections/{collection_id}/items/{item_id}","method":["GET"]},{"path":"/search","method":["GET","POST"]},{"path":"/collections","method":["GET"]},{"path":"/collections/{collection_id}","method":["GET"]},{"path":"/collections/{collection_id}/items","method":["GET"]},{"path":"/queryables","method":["GET"]},{"path":"/queryables/collections/{collection_id}/queryables","method":["GET"]},{"path":"/_mgmt/ping","method":["GET"]}],"dependencies":[{"method":"stac_fastapi.core.basic_auth.BasicAuth","kwargs":{"credentials":[{"username":"reader","password":"reader"}]}}]}] + - STAC_FASTAPI_ROUTE_DEPENDENCIES=[{"routes":[{"method":"*","path":"*"}],"dependencies":[{"method":"stac_fastapi.core.basic_auth.BasicAuth","kwargs":{"credentials":[{"username":"admin","password":"admin"}]}}]},{"routes":[{"path":"/","method":["GET"]},{"path":"/conformance","method":["GET"]},{"path":"/collections/{collection_id}/items/{item_id}","method":["GET"]},{"path":"/search","method":["GET","POST"]},{"path":"/collections","method":["GET"]},{"path":"/collections/{collection_id}","method":["GET"]},{"path":"/collections/{collection_id}/items","method":["GET"]},{"path":"/queryables","method":["GET"]},{"path":"/collections/{collection_id}/queryables","method":["GET"]},{"path":"/_mgmt/ping","method":["GET"]}],"dependencies":[{"method":"stac_fastapi.core.basic_auth.BasicAuth","kwargs":{"credentials":[{"username":"reader","password":"reader"}]}}]}] ports: - "8082:8082" volumes: diff --git a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py index cda5a464..8039264d 100644 --- a/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py +++ b/stac_fastapi/elasticsearch/stac_fastapi/elasticsearch/app.py @@ -104,22 +104,24 @@ post_request_model = create_post_request_model(search_extensions) -api = StacApi( - title=os.getenv("STAC_FASTAPI_TITLE", "stac-fastapi-elasticsearch"), - description=os.getenv("STAC_FASTAPI_DESCRIPTION", "stac-fastapi-elasticsearch"), - api_version=os.getenv("STAC_FASTAPI_VERSION", "5.0.0a1"), - settings=settings, - extensions=extensions, - client=CoreClient( +app_config = { + "title": os.getenv("STAC_FASTAPI_TITLE", "stac-fastapi-elasticsearch"), + "description": os.getenv("STAC_FASTAPI_DESCRIPTION", "stac-fastapi-elasticsearch"), + "api_version": os.getenv("STAC_FASTAPI_VERSION", "5.0.0a1"), + "settings": settings, + "extensions": extensions, + "client": CoreClient( database=database_logic, session=session, post_request_model=post_request_model, landing_page_id=os.getenv("STAC_FASTAPI_LANDING_PAGE_ID", "stac-fastapi"), ), - search_get_request_model=create_get_request_model(search_extensions), - search_post_request_model=post_request_model, - route_dependencies=get_route_dependencies(), -) + "search_get_request_model": create_get_request_model(search_extensions), + "search_post_request_model": post_request_model, + "route_dependencies": get_route_dependencies(), +} + +api = StacApi(**app_config) @asynccontextmanager diff --git a/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py index 66671ff0..54ed8ac3 100644 --- a/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py +++ b/stac_fastapi/opensearch/stac_fastapi/opensearch/app.py @@ -105,22 +105,24 @@ post_request_model = create_post_request_model(search_extensions) -api = StacApi( - title=os.getenv("STAC_FASTAPI_TITLE", "stac-fastapi-opensearch"), - description=os.getenv("STAC_FASTAPI_DESCRIPTION", "stac-fastapi-opensearch"), - api_version=os.getenv("STAC_FASTAPI_VERSION", "5.0.0a1"), - settings=settings, - extensions=extensions, - client=CoreClient( +app_config = { + "title": os.getenv("STAC_FASTAPI_TITLE", "stac-fastapi-opensearch"), + "description": os.getenv("STAC_FASTAPI_DESCRIPTION", "stac-fastapi-opensearch"), + "api_version": os.getenv("STAC_FASTAPI_VERSION", "5.0.0a1"), + "settings": settings, + "extensions": extensions, + "client": CoreClient( database=database_logic, session=session, post_request_model=post_request_model, landing_page_id=os.getenv("STAC_FASTAPI_LANDING_PAGE_ID", "stac-fastapi"), ), - search_get_request_model=create_get_request_model(search_extensions), - search_post_request_model=post_request_model, - route_dependencies=get_route_dependencies(), -) + "search_get_request_model": create_get_request_model(search_extensions), + "search_post_request_model": post_request_model, + "route_dependencies": get_route_dependencies(), +} + +api = StacApi(**app_config) @asynccontextmanager diff --git a/stac_fastapi/tests/api/test_api.py b/stac_fastapi/tests/api/test_api.py index 807da5e4..c5cb6415 100644 --- a/stac_fastapi/tests/api/test_api.py +++ b/stac_fastapi/tests/api/test_api.py @@ -34,6 +34,7 @@ "POST /collections/{collection_id}/items", "PUT /collections/{collection_id}", "PUT /collections/{collection_id}/items/{item_id}", + "POST /collections/{collection_id}/bulk_items", "GET /aggregations", "GET /aggregate", "POST /aggregations", diff --git a/stac_fastapi/tests/conftest.py b/stac_fastapi/tests/conftest.py index 7d8c1113..a1761288 100644 --- a/stac_fastapi/tests/conftest.py +++ b/stac_fastapi/tests/conftest.py @@ -2,7 +2,6 @@ import copy import json import os -import sys from typing import Any, Callable, Dict, Optional import pytest @@ -13,7 +12,7 @@ from stac_pydantic import api from stac_fastapi.api.app import StacApi -from stac_fastapi.api.models import create_get_request_model, create_post_request_model +from stac_fastapi.core.basic_auth import BasicAuth from stac_fastapi.core.core import ( BulkTransactionsClient, CoreClient, @@ -25,13 +24,11 @@ EsAggregationExtensionPostRequest, ) from stac_fastapi.core.rate_limit import setup_rate_limit -from stac_fastapi.core.route_dependencies import get_route_dependencies from stac_fastapi.core.utilities import get_bool_env -from stac_fastapi.extensions.core.filter import FilterConformanceClasses from stac_fastapi.sfeos_helpers.aggregation import EsAsyncBaseAggregationClient -from stac_fastapi.sfeos_helpers.filter import EsAsyncBaseFiltersClient if os.getenv("BACKEND", "elasticsearch").lower() == "opensearch": + from stac_fastapi.opensearch.app import app_config from stac_fastapi.opensearch.config import AsyncOpensearchSettings as AsyncSettings from stac_fastapi.opensearch.config import OpensearchSettings as SearchSettings from stac_fastapi.opensearch.database_logic import ( @@ -40,6 +37,7 @@ create_index_templates, ) else: + from stac_fastapi.elasticsearch.app import app_config from stac_fastapi.elasticsearch.config import ( AsyncElasticsearchSettings as AsyncSettings, ) @@ -200,54 +198,7 @@ def bulk_txn_client(): @pytest_asyncio.fixture(scope="session") async def app(): - settings = AsyncSettings() - - filter_extension = FilterExtension( - client=EsAsyncBaseFiltersClient(database=database) - ) - filter_extension.conformance_classes.append( - FilterConformanceClasses.ADVANCED_COMPARISON_OPERATORS - ) - - aggregation_extension = AggregationExtension( - client=EsAsyncBaseAggregationClient( - database=database, session=None, settings=settings - ) - ) - aggregation_extension.POST = EsAggregationExtensionPostRequest - aggregation_extension.GET = EsAggregationExtensionGetRequest - - search_extensions = [ - TransactionExtension( - client=TransactionsClient( - database=database, session=None, settings=settings - ), - settings=settings, - ), - SortExtension(), - FieldsExtension(), - QueryExtension(), - TokenPaginationExtension(), - filter_extension, - FreeTextExtension(), - ] - - extensions = [aggregation_extension] + search_extensions - - post_request_model = create_post_request_model(search_extensions) - - return StacApi( - settings=settings, - client=CoreClient( - database=database, - session=None, - extensions=extensions, - post_request_model=post_request_model, - ), - extensions=extensions, - search_get_request_model=create_get_request_model(search_extensions), - search_post_request_model=post_request_model, - ).app + return StacApi(**app_config).app @pytest_asyncio.fixture(scope="session") @@ -263,49 +214,8 @@ async def app_client(app): @pytest_asyncio.fixture(scope="session") async def app_rate_limit(): - settings = AsyncSettings() - - aggregation_extension = AggregationExtension( - client=EsAsyncBaseAggregationClient( - database=database, session=None, settings=settings - ) - ) - aggregation_extension.POST = EsAggregationExtensionPostRequest - aggregation_extension.GET = EsAggregationExtensionGetRequest - - search_extensions = [ - TransactionExtension( - client=TransactionsClient( - database=database, session=None, settings=settings - ), - settings=settings, - ), - SortExtension(), - FieldsExtension(), - QueryExtension(), - TokenPaginationExtension(), - FilterExtension(), - FreeTextExtension(), - ] - - extensions = [aggregation_extension] + search_extensions - - post_request_model = create_post_request_model(search_extensions) - - app = StacApi( - settings=settings, - client=CoreClient( - database=database, - session=None, - extensions=extensions, - post_request_model=post_request_model, - ), - extensions=extensions, - search_get_request_model=create_get_request_model(search_extensions), - search_post_request_model=post_request_model, - ).app - - # Set up rate limit + """Fixture to get the FastAPI app with test-specific rate limiting.""" + app = StacApi(**app_config).app setup_rate_limit(app, rate_limit="2/minute") return app @@ -324,82 +234,52 @@ async def app_client_rate_limit(app_rate_limit): @pytest_asyncio.fixture(scope="session") async def app_basic_auth(): - stac_fastapi_route_dependencies = """[ - { - "routes":[{"method":"*","path":"*"}], - "dependencies":[ - { - "method":"stac_fastapi.core.basic_auth.BasicAuth", - "kwargs":{"credentials":[{"username":"admin","password":"admin"}]} - } - ] - }, - { - "routes":[ - {"path":"/","method":["GET"]}, - {"path":"/conformance","method":["GET"]}, - {"path":"/collections/{collection_id}/items/{item_id}","method":["GET"]}, - {"path":"/search","method":["GET","POST"]}, - {"path":"/collections","method":["GET"]}, - {"path":"/collections/{collection_id}","method":["GET"]}, - {"path":"/collections/{collection_id}/items","method":["GET"]}, - {"path":"/queryables","method":["GET"]}, - {"path":"/queryables/collections/{collection_id}/queryables","method":["GET"]}, - {"path":"/_mgmt/ping","method":["GET"]} - ], - "dependencies":[ - { - "method":"stac_fastapi.core.basic_auth.BasicAuth", - "kwargs":{"credentials":[{"username":"reader","password":"reader"}]} - } - ] - } - ]""" + """Fixture to get the FastAPI app with basic auth configured.""" - settings = AsyncSettings() + # Create a copy of the app config + test_config = app_config.copy() - aggregation_extension = AggregationExtension( - client=EsAsyncBaseAggregationClient( - database=database, session=None, settings=settings - ) + # Create basic auth dependency wrapped in Depends + basic_auth = Depends( + BasicAuth(credentials=[{"username": "admin", "password": "admin"}]) ) - aggregation_extension.POST = EsAggregationExtensionPostRequest - aggregation_extension.GET = EsAggregationExtensionGetRequest - search_extensions = [ - TransactionExtension( - client=TransactionsClient( - database=database, session=None, settings=settings - ), - settings=settings, - ), - SortExtension(), - FieldsExtension(), - QueryExtension(), - TokenPaginationExtension(), - FilterExtension(), - FreeTextExtension(), + # Define public routes that don't require auth + public_paths = { + "/": ["GET"], + "/conformance": ["GET"], + "/collections/{collection_id}/items/{item_id}": ["GET"], + "/search": ["GET", "POST"], + "/collections": ["GET"], + "/collections/{collection_id}": ["GET"], + "/collections/{collection_id}/items": ["GET"], + "/queryables": ["GET"], + "/collections/{collection_id}/queryables": ["GET"], + "/_mgmt/ping": ["GET"], + } + + # Initialize route dependencies with public paths + test_config["route_dependencies"] = [ + ( + [{"path": path, "method": method} for method in methods], + [], # No auth for public routes + ) + for path, methods in public_paths.items() ] - extensions = [aggregation_extension] + search_extensions - - post_request_model = create_post_request_model(search_extensions) - - stac_api = StacApi( - settings=settings, - client=CoreClient( - database=database, - session=None, - extensions=extensions, - post_request_model=post_request_model, - ), - extensions=extensions, - search_get_request_model=create_get_request_model(search_extensions), - search_post_request_model=post_request_model, - route_dependencies=get_route_dependencies(stac_fastapi_route_dependencies), + # Add catch-all route with basic auth + test_config["route_dependencies"].extend( + [ + ( + [{"path": "*", "method": "*"}], + [basic_auth], + ) # Require auth for all other routes + ] ) - return stac_api.app + # Create the app with basic auth + api = StacApi(**test_config) + return api.app @pytest_asyncio.fixture(scope="session") @@ -428,56 +308,19 @@ def must_be_bob( @pytest_asyncio.fixture(scope="session") async def route_dependencies_app(): - # Add file to python path to allow get_route_dependencies to import must_be_bob - sys.path.append(os.path.dirname(__file__)) - - stac_fastapi_route_dependencies = """[ - { - "routes": [ - { - "method": "GET", - "path": "/collections" - } - ], - "dependencies": [ - { - "method": "conftest.must_be_bob" - } - ] - } - ]""" + """Fixture to get the FastAPI app with custom route dependencies.""" - settings = AsyncSettings() - extensions = [ - TransactionExtension( - client=TransactionsClient( - database=database, session=None, settings=settings - ), - settings=settings, - ), - SortExtension(), - FieldsExtension(), - QueryExtension(), - TokenPaginationExtension(), - FilterExtension(), - FreeTextExtension(), - ] + # Create a copy of the app config + test_config = app_config.copy() - post_request_model = create_post_request_model(extensions) + # Define route dependencies + test_config["route_dependencies"] = [ + ([{"method": "GET", "path": "/collections"}], [Depends(must_be_bob)]) + ] - return StacApi( - settings=settings, - client=CoreClient( - database=database, - session=None, - extensions=extensions, - post_request_model=post_request_model, - ), - extensions=extensions, - search_get_request_model=create_get_request_model(extensions), - search_post_request_model=post_request_model, - route_dependencies=get_route_dependencies(stac_fastapi_route_dependencies), - ).app + # Create the app with custom route dependencies + api = StacApi(**test_config) + return api.app @pytest_asyncio.fixture(scope="session") @@ -493,9 +336,16 @@ async def route_dependencies_client(route_dependencies_app): def build_test_app(): + """Build a test app with configurable transaction extensions.""" + # Create a copy of the base config + test_config = app_config.copy() + + # Get transaction extensions setting TRANSACTIONS_EXTENSIONS = get_bool_env( "ENABLE_TRANSACTIONS_EXTENSIONS", default=True ) + + # Configure extensions settings = AsyncSettings() aggregation_extension = AggregationExtension( client=EsAsyncBaseAggregationClient( @@ -504,6 +354,7 @@ def build_test_app(): ) aggregation_extension.POST = EsAggregationExtensionPostRequest aggregation_extension.GET = EsAggregationExtensionGetRequest + search_extensions = [ SortExtension(), FieldsExtension(), @@ -512,27 +363,30 @@ def build_test_app(): FilterExtension(), FreeTextExtension(), ] + + # Add transaction extension if enabled if TRANSACTIONS_EXTENSIONS: - search_extensions.insert( - 0, + search_extensions.append( TransactionExtension( client=TransactionsClient( database=database, session=None, settings=settings ), settings=settings, - ), + ) ) + + # Update extensions in config extensions = [aggregation_extension] + search_extensions - post_request_model = create_post_request_model(search_extensions) - return StacApi( - settings=settings, - client=CoreClient( - database=database, - session=None, - extensions=extensions, - post_request_model=post_request_model, - ), + test_config["extensions"] = extensions + + # Update client with new extensions + test_config["client"] = CoreClient( + database=database, + session=None, extensions=extensions, - search_get_request_model=create_get_request_model(search_extensions), - search_post_request_model=post_request_model, - ).app + post_request_model=test_config["search_post_request_model"], + ) + + # Create and return the app + api = StacApi(**test_config) + return api.app