diff --git a/API/auth/__init__.py b/API/auth/__init__.py index 0ff71d79..1cfc1417 100644 --- a/API/auth/__init__.py +++ b/API/auth/__init__.py @@ -24,10 +24,12 @@ class AuthUser(BaseModel): osm_auth = Auth(*get_oauth_credentials()) - def get_user_from_db(osm_id: int): auth = Users() user = auth.read_user(osm_id) + # Add changes that when the User is not found in the Database 404 Error is raised + if not user: + raise HTTPException(status_code=404, detail="User not Found in the Database") return user diff --git a/API/auth/responses.py b/API/auth/responses.py new file mode 100644 index 00000000..c5450e50 --- /dev/null +++ b/API/auth/responses.py @@ -0,0 +1,24 @@ +from fastapi import HTTPException +from pydantic import BaseModel + +# Define common error responses +common_error_responses = { + 403: {"description": "Forbidden", "model": HTTPException}, + 404: {"description": "Not Found", "model": HTTPException}, + 500: {"description": "Internal Server Error", "model": HTTPException}, +} + +# Define shared error response models +class ErrorResponse(BaseModel): + detail: str + +# Add a default response model for error responses +for status_code in common_error_responses: + if "model" not in common_error_responses[status_code]: + common_error_responses[status_code]["model"] = ErrorResponse + +error_responses_with_examples = { + 403: {"content": {"application/json": {"example": {"message": "Access forbidden"}}}}, + 404: {"content": {"application/json": {"example": {"error": "Not Found"}}}}, + 500: {"content": {"application/json": {"example": {"error": "Internal Server Error"}}}}, +} diff --git a/API/auth/routers.py b/API/auth/routers.py index 438a28e4..05e18476 100644 --- a/API/auth/routers.py +++ b/API/auth/routers.py @@ -1,16 +1,19 @@ import json -from fastapi import APIRouter, Depends, Request +from fastapi import APIRouter, Depends, Request, HTTPException from pydantic import BaseModel from src.app import Users from . import AuthUser, admin_required, login_required, osm_auth, staff_required +from .responses import common_error_responses, error_responses_with_examples router = APIRouter(prefix="/auth", tags=["Auth"]) -@router.get("/login/") +@router.get( + "/login", responses={**common_error_responses, **error_responses_with_examples} +) def login_url(request: Request): """Generate Login URL for authentication using OAuth2 Application registered with OpenStreetMap. Click on the download url returned to get access_token. @@ -25,7 +28,9 @@ def login_url(request: Request): return login_url -@router.get("/callback/") +@router.get( + "/callback", responses={**common_error_responses, **error_responses_with_examples} +) def callback(request: Request): """Performs token exchange between OpenStreetMap and Raw Data API @@ -37,12 +42,22 @@ def callback(request: Request): Returns: - access_token (string) """ - access_token = osm_auth.callback(str(request.url)) + try: + access_token = osm_auth.callback(str(request.url)) + except Exception as ex: + raise HTTPException( + status_code=500, + detail="Internal Server Error occurred while performing token exchange between OpenStreetMap and Raw Data API", + ) return access_token -@router.get("/me/", response_model=AuthUser) +@router.get( + "/me", + response_model=AuthUser, + responses={**common_error_responses, **error_responses_with_examples}, +) def my_data(user_data: AuthUser = Depends(login_required)): """Read the access token and provide user details from OSM user's API endpoint, also integrated with underpass . @@ -64,7 +79,11 @@ class User(BaseModel): # Create user -@router.post("/users/", response_model=dict) +@router.post( + "/users", + response_model=dict, + responses={**common_error_responses, **error_responses_with_examples}, +) async def create_user(params: User, user_data: AuthUser = Depends(admin_required)): """ Creates a new user and returns the user's information. @@ -87,7 +106,11 @@ async def create_user(params: User, user_data: AuthUser = Depends(admin_required # Read user by osm_id -@router.get("/users/{osm_id}", response_model=dict) +@router.get( + "/users/{osm_id}", + response_model=dict, + responses={**common_error_responses, **error_responses_with_examples}, +) async def read_user(osm_id: int, user_data: AuthUser = Depends(staff_required)): """ Retrieves user information based on the given osm_id. @@ -103,7 +126,8 @@ async def read_user(osm_id: int, user_data: AuthUser = Depends(staff_required)): - Dict[str, Any]: A dictionary containing user information. Raises: - - HTTPException: If the user with the given osm_id is not found. + - HTTPException 404: If the user with the given osm_id is not found. + - HTTPException 403: If the user is not a staff. """ auth = Users() @@ -111,7 +135,11 @@ async def read_user(osm_id: int, user_data: AuthUser = Depends(staff_required)): # Update user by osm_id -@router.put("/users/{osm_id}", response_model=dict) +@router.put( + "/users/{osm_id}", + response_model=dict, + responses={**common_error_responses, **error_responses_with_examples}, +) async def update_user( osm_id: int, update_data: User, user_data: AuthUser = Depends(admin_required) ): @@ -129,14 +157,19 @@ async def update_user( - Dict[str, Any]: A dictionary containing the updated user information. Raises: - - HTTPException: If the user with the given osm_id is not found. + - HTTPException 403: If the user is not an Admin. + - HTTPException 404: If the user with the given osm_id is not found. """ auth = Users() return auth.update_user(osm_id, update_data) # Delete user by osm_id -@router.delete("/users/{osm_id}", response_model=dict) +@router.delete( + "/users/{osm_id}", + response_model=dict, + responses={**common_error_responses, **error_responses_with_examples}, +) async def delete_user(osm_id: int, user_data: AuthUser = Depends(admin_required)): """ Deletes a user based on the given osm_id. @@ -148,14 +181,19 @@ async def delete_user(osm_id: int, user_data: AuthUser = Depends(admin_required) - Dict[str, Any]: A dictionary containing the deleted user information. Raises: - - HTTPException: If the user with the given osm_id is not found. + - HTTPException 404: If the user with the given osm_id is not found. + - HTTPException 403: If the user is not an Admin. """ auth = Users() return auth.delete_user(osm_id) # Get all users -@router.get("/users/", response_model=list) +@router.get( + "/users", + response_model=list, + responses={**common_error_responses, **error_responses_with_examples}, +) async def read_users( skip: int = 0, limit: int = 10, user_data: AuthUser = Depends(staff_required) ): @@ -168,6 +206,9 @@ async def read_users( Returns: - List[Dict[str, Any]]: A list of dictionaries containing user information. + + Raises: + - HTTPException 403: If the user is not a Staff. """ auth = Users() return auth.read_users(skip, limit) diff --git a/API/custom_exports.py b/API/custom_exports.py index da4bcea1..75127ab5 100644 --- a/API/custom_exports.py +++ b/API/custom_exports.py @@ -12,8 +12,7 @@ router = APIRouter(prefix="/custom", tags=["Custom Exports"]) - -@router.post("/snapshot/") +@router.post("/snapshot") @limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute") @version(1) async def process_custom_requests( diff --git a/API/hdx.py b/API/hdx.py index b5d89a20..36618d51 100644 --- a/API/hdx.py +++ b/API/hdx.py @@ -70,7 +70,7 @@ async def read_hdx_list( return hdx_list -@router.get("/search/", response_model=List[dict]) +@router.get("/search", response_model=List[dict]) @limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute") @version(1) async def search_hdx( diff --git a/API/main.py b/API/main.py index f887895a..496ace96 100644 --- a/API/main.py +++ b/API/main.py @@ -75,7 +75,12 @@ os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" -app = FastAPI(title="Raw Data API ", swagger_ui_parameters={"syntaxHighlight": False}) +# Provides a brief overview of the API's functionality for documentation purposes. +app = FastAPI( + title="Raw Data API", + description="Raw Data API is a set of high-performant APIs for transforming and exporting OpenStreetMap (OSM) data in different GIS file formats.", + swagger_ui_parameters={"syntaxHighlight": False} +) app.include_router(auth_router) app.include_router(raw_data_router) app.include_router(tasks_router) diff --git a/API/raw_data.py b/API/raw_data.py index 46c8e068..612c2e92 100644 --- a/API/raw_data.py +++ b/API/raw_data.py @@ -26,6 +26,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Request from fastapi.responses import JSONResponse from fastapi_versioning import version +from .auth.responses import common_error_responses, error_responses_with_examples from src.app import RawData from src.config import ( @@ -51,7 +52,11 @@ redis_client = redis.StrictRedis.from_url(CELERY_BROKER_URL) -@router.get("/status/", response_model=StatusResponse) +@router.get( + "/status", + response_model=StatusResponse, + responses={**common_error_responses, **error_responses_with_examples}, +) @version(1) def check_database_last_updated(): """Gives status about how recent the osm data is , it will give the last time that database was updated completely""" @@ -59,7 +64,11 @@ def check_database_last_updated(): return {"last_updated": result} -@router.post("/snapshot/", response_model=SnapshotResponse) +@router.post( + "/snapshot", + response_model=SnapshotResponse, + responses={**common_error_responses, **error_responses_with_examples}, +) @limiter.limit(f"{export_rate_limit}/minute") @version(1) def get_osm_current_snapshot_as_file( @@ -462,7 +471,10 @@ def get_osm_current_snapshot_as_file( ) -@router.post("/snapshot/plain/") +@router.post( + "/snapshot/plain", + responses={**common_error_responses, **error_responses_with_examples}, +) @version(1) def get_osm_current_snapshot_as_plain_geojson( request: Request, @@ -494,14 +506,18 @@ def get_osm_current_snapshot_as_plain_geojson( return result -@router.get("/countries/") +@router.get( + "/countries", responses={**common_error_responses, **error_responses_with_examples} +) @version(1) def get_countries(q: str = ""): result = RawData().get_countries_list(q) return result -@router.get("/osm_id/") +@router.get( + "/osm_id", responses={**common_error_responses, **error_responses_with_examples} +) @version(1) def get_osm_feature(osm_id: int): return RawData().get_osm_feature(osm_id) diff --git a/API/s3.py b/API/s3.py index 767f2952..708cc52d 100644 --- a/API/s3.py +++ b/API/s3.py @@ -32,7 +32,7 @@ paginator = s3.get_paginator("list_objects_v2") -@router.get("/files/") +@router.get("/files") @limiter.limit(f"{RATE_LIMIT_PER_MIN}/minute") @version(1) async def list_s3_files( diff --git a/API/stats.py b/API/stats.py index b2ca5414..c8ebd6ee 100644 --- a/API/stats.py +++ b/API/stats.py @@ -11,7 +11,7 @@ router = APIRouter(prefix="/stats", tags=["Stats"]) -@router.post("/polygon/") +@router.post("/polygon") @limiter.limit(f"{POLYGON_STATISTICS_API_RATE_LIMIT}/minute") @version(1) async def get_polygon_stats( diff --git a/API/tasks.py b/API/tasks.py index bca37813..6ec90e57 100644 --- a/API/tasks.py +++ b/API/tasks.py @@ -12,11 +12,17 @@ from .api_worker import celery from .auth import AuthUser, admin_required, login_required, staff_required +from .auth.responses import common_error_responses, error_responses_with_examples + router = APIRouter(prefix="/tasks", tags=["Tasks"]) -@router.get("/status/{task_id}/", response_model=SnapshotTaskResponse) +@router.get( + "/status/{task_id}", + response_model=SnapshotTaskResponse, + responses={**common_error_responses, **error_responses_with_examples}, +) @version(1) def get_task_status( task_id, @@ -78,7 +84,10 @@ def get_task_status( return JSONResponse(result) -@router.get("/revoke/{task_id}/") +@router.get( + "/revoke/{task_id}", + responses={**common_error_responses, **error_responses_with_examples}, +) @version(1) def revoke_task(task_id, user: AuthUser = Depends(staff_required)): """Revokes task , Terminates if it is executing @@ -88,12 +97,16 @@ def revoke_task(task_id, user: AuthUser = Depends(staff_required)): Returns: id: id of revoked task + Raises: + - HTTPException 403: If the user is not an Staff. """ celery.control.revoke(task_id=task_id, terminate=True) return JSONResponse({"id": task_id}) -@router.get("/inspect/") +@router.get( + "/inspect", responses={**common_error_responses, **error_responses_with_examples} +) @version(1) def inspect_workers( request: Request, @@ -134,7 +147,9 @@ def inspect_workers( return JSONResponse(content=response_data) -@router.get("/ping/") +@router.get( + "/ping", responses={**common_error_responses, **error_responses_with_examples} +) @version(1) def ping_workers(): """Pings available workers @@ -145,12 +160,18 @@ def ping_workers(): return JSONResponse(inspected_ping) -@router.get("/purge/") +@router.get( + "/purge", responses={**common_error_responses, **error_responses_with_examples} +) @version(1) def discard_all_waiting_tasks(user: AuthUser = Depends(admin_required)): """ Discards all waiting tasks from the queue Returns : Number of tasks discarded + + Raises: + - HTTPException 403: If the user is not an Admin. + """ purged = celery.control.purge() return JSONResponse({"tasks_discarded": purged}) @@ -159,7 +180,9 @@ def discard_all_waiting_tasks(user: AuthUser = Depends(admin_required)): queues = [DEFAULT_QUEUE_NAME, DAEMON_QUEUE_NAME] -@router.get("/queue/") +@router.get( + "/queue", responses={**common_error_responses, **error_responses_with_examples} +) @version(1) def get_queue_info(): queue_info = {} @@ -176,7 +199,10 @@ def get_queue_info(): return JSONResponse(content=queue_info) -@router.get("/queue/details/{queue_name}/") +@router.get( + "/queue/details/{queue_name}", + responses={**common_error_responses, **error_responses_with_examples}, +) @version(1) def get_list_details( queue_name: str, diff --git a/README.md b/README.md index b97ae50e..460a98d2 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,38 @@ py.test -v -s py.test -k test function name ``` +### Running Tests in Docker Container + +To run tests in Docker locally, follow these steps: + +1. **Update .dockerignore File:** + - Open the `.dockerignore` file in your project. + - Comment out the line that excludes the `tests` directory. This allows Docker to include the test files in the container. + +2. **Modify Dockerfile:** + - Navigate to your Dockerfile. + - Add the following line to copy the tests directory into the Docker container: + ``` + COPY tests/ ./tests/ + ``` + +3. **Spin up the Containers:** + - Use Docker Compose to manage the containers. + - Run the following command to build and start the containers in detached mode: + ``` + docker-compose up -d --build + ``` +4. **Run a Bash Shell Inside the Container:** + + - To access a Bash shell inside a Docker container, use the following command: + +```docker exec -it CONTAINER_NAME /bin/bash``` +Then Run the above commands to run tests + +By following these steps, you'll be able to run your tests inside a Docker container locally. Make sure to check the test results to ensure everything is working as expected. + +**Note:** Make sure you have exported the `PYTHONPATH` and `ACCESS_TOKEN` environment variables in your terminal before running the tests. For detailed instructions on installation using Docker, refer to the [Docker Installation Guide: Setting Environment Variables in Docker Container](https://github.com/hotosm/raw-data-api/blob/develop/docs/src/installation/docker.md). + ## Contribution & Development Learn about current priorities and work going through Roadmap & see here [CONTRIBUTING](./docs/src/contributing.md) diff --git a/docs/src/installation/docker.md b/docs/src/installation/docker.md index 29cb0308..61a0c4fe 100644 --- a/docs/src/installation/docker.md +++ b/docs/src/installation/docker.md @@ -45,6 +45,27 @@ You can either use full composed docker-compose directly or you can build docker docker-compose up -d --build ``` +### Setting Environment Variables in Docker Container + +To configure the environment variables `PYTHONPATH` and `ACCESS_TOKEN` in your Docker container, follow the steps below: + +#### PYTHONPATH + +- The `PYTHONPATH` environment variable specifies the directories where Python looks for modules and packages. +- In the context of your Docker container, setting `PYTHONPATH` to the present working directory (`pwd`) means that Python will search for modules and packages in the current directory. +- This is particularly useful when you have custom modules or packages in your project that you want Python to recognize and import. + +#### ACCESS_TOKEN + +- `ACCESS_TOKEN` is an environment variable used for authentication purposes. +- To obtain the `ACCESS_TOKEN`, you need to generate a login URL for authentication using an OAuth2 application registered with OpenStreetMap. +- Click on the generated URL, which will redirect you to the OpenStreetMap authentication page. +- After logging in and authorizing the OAuth2 application, OpenStreetMap will provide an `ACCESS_TOKEN`. +- Set the obtained `ACCESS_TOKEN` as an environment variable in the Docker container. +- This allows your application to use the token for making authenticated requests to OpenStreetMap APIs. + +In summary, configuring the `PYTHONPATH` to the present working directory enables Python to find modules and packages in your project, while obtaining the `ACCESS_TOKEN` involves generating a login URL for OAuth2 authentication with OpenStreetMap and setting the resulting token as an environment variable in the Docker container for authentication purposes. + OR ### Run Docker without docker compose for development diff --git a/tests/test_API.py b/tests/test_API.py index 163cbc00..f7b60c54 100644 --- a/tests/test_API.py +++ b/tests/test_API.py @@ -1,3 +1,51 @@ +""" +FastAPI test client for the Raw Data API +======================================== + +This script contains test functions for the Raw Data API, which provides +various functionalities such as status checks, login, country information, +snapshot generation, and task management. + +To run the tests, simply execute this script using a Python interpreter. + +Note: An access token is required to run most of the tests. + +Prerequisites +============ + +- FastAPI test client +- Access token for Raw Data API + +Test Functions +============= + +The following test functions are available: + +- `test_status()`: Checks the status of the Raw Data API +- `test_login_url()`: Checks the login URL +- `test_login_auth_me()`: Checks the authentication status +- `test_countries_endpoint()`: Queries country information for Nepal +- `test_osm_id_endpoint()`: Queries OSM ID information +- `test_snapshot()`: Generates a snapshot of a polygon +- `test_snapshot_featurecollection()`: Generates a snapshot of a feature collection +- `test_snapshot_feature()`: Generates a snapshot of a feature +- `test_snapshot_feature_fgb()`: Generates a snapshot of a feature in FGB format +- `test_snapshot_feature_fgb_wrap_geom()`: Generates a snapshot of a feature in FGB format with wrapped geometry +- `test_snapshot_feature_shp()`: Generates a snapshot of a feature in SHP format +- `test_snapshot_feature_gpkg()`: Generates a snapshot of a feature in GPKG format +- `test_snapshot_feature_kml()`: Generates a snapshot of a feature in KML format +- `test_snapshot_feature_sql()`: Generates a snapshot of a feature in SQL format +- `test_snapshot_feature_csv()`: Generates a snapshot of a feature in CSV format +- `test_snapshot_centroid()`: Generates a snapshot of a polygon centroid +- `test_snapshot_filters()`: Generates a snapshot of a custom polygon with filters +- `test_snapshot_filters_and_filter()`: Generates a snapshot of a custom polygon with filters and AND operator +- `test_snapshot_and_filter()`: Generates a snapshot of a custom polygon with filters and AND/OR operators +- `test_snapshot_authentication_uuid()`: Generates a snapshot with UUID authentication +- `test_snapshot_bind_zip()`: Generates a snapshot with ZIP binding +- `test_worker_connection()`: Checks the connection to the worker + +""" + import os import time @@ -39,26 +87,40 @@ def wait_for_task_completion(track_link, max_attempts=12, interval_seconds=5): def test_status(): + """ + Checks the status of the Raw Data API + """ response = client.get("/v1/status/") assert response.status_code == 200 ## Login def test_login_url(): + """ + Checks the login URL + """ response = client.get("/v1/auth/login/") assert response.status_code == 200 def test_login_auth_me(): - headers = {"access-token": access_token} - response = client.get("/v1/auth/me/", headers=headers) - assert response.status_code == 200 - + """ + Checks the authentication status + """ + if access_token is None: + raise Exception("Access token is not available. Cannot execute tests.") + else: + headers = {"access-token": access_token} + response = client.get("/v1/auth/me/", headers=headers) + assert response.status_code == 200 ## Countries def test_countries_endpoint(): + """ + Queries country information for Nepal + """ response = client.get("/v1/countries/?q=nepal") assert response.status_code == 200 @@ -67,12 +129,18 @@ def test_countries_endpoint(): def test_osm_id_endpoint(): + """ + Queries OSM ID information + """ response = client.get("/v1/osm_id/?osm_id=421498318") assert response.status_code == 200 ## Snapshot def test_snapshot(): + """ + Generates a snapshot of a polygon + """ response = client.post( "/v1/snapshot/", json={ @@ -97,6 +165,10 @@ def test_snapshot(): def test_snapshot_featurecollection(): + """ + Test creating a snapshot of a specific geographic area. + Assert 200 status code, retrieve 'track_link' and wait for task completion. + """ response = client.post( "/v1/snapshot/", json={ @@ -130,6 +202,12 @@ def test_snapshot_featurecollection(): def test_snapshot_feature(): + """ + Test creating a snapshot of a geographic area. + Send a POST request with a Feature payload, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -158,6 +236,12 @@ def test_snapshot_feature(): def test_snapshot_feature_fgb(): + """ + Test creating a snapshot of a geographic area with output type 'fgb'. + Send a POST request with a Feature payload, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -187,6 +271,12 @@ def test_snapshot_feature_fgb(): def test_snapshot_feature_fgb_wrap_geom(): + """ + Test creating a snapshot of a geographic area with output type 'fgb'. + Send a POST request with a Feature payload, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -217,6 +307,12 @@ def test_snapshot_feature_fgb_wrap_geom(): def test_snapshot_feature_shp(): + """ + Test creating a snapshot of a geographic area with output type 'shp'. + Send a POST request with a Feature payload, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -246,6 +342,12 @@ def test_snapshot_feature_shp(): def test_snapshot_feature_gpkg(): + """ + Test creating a snapshot of a geographic area with output type 'gpkg'. + Send a POST request with a Feature payload, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -275,6 +377,12 @@ def test_snapshot_feature_gpkg(): def test_snapshot_feature_kml(): + """ + Test creating a snapshot of a geographic area with output type 'kml'. + Send a POST request with a Feature payload, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -304,6 +412,19 @@ def test_snapshot_feature_kml(): def test_snapshot_feature_sql(): + """ + Test creating an SQL formatted snapshot for a GeoJSON Feature object. + + Send a POST request to the "/v1/snapshot/" endpoint, + including the target output format and a GeoJSON Feature + object in the request body. + + Assert that: + - the status code is 200 + - data is retrieved + - the track_link is assigned + - the task is completed + """ response = client.post( "/v1/snapshot/", json={ @@ -333,6 +454,12 @@ def test_snapshot_feature_sql(): def test_snapshot_feature_csv(): + """ + Test creating a snapshot of a geographic area with output type 'sql'. + Send a POST request with a Feature payload, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -362,6 +489,12 @@ def test_snapshot_feature_csv(): def test_snapshot_centroid(): + """ + Test creating a snapshot of a geographic area with centroid calculation. + Send a POST request with a Polygon payload and centroid=True, + expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -387,6 +520,14 @@ def test_snapshot_centroid(): def test_snapshot_filters(): + """ + Test creating a snapshot with tags filter and specific attributes. + Send a POST request with a Polygon payload, + including tags for points, lines and polygons, + and a list of requested attributes. + Expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -613,6 +754,14 @@ def test_snapshot_filters(): def test_snapshot_and_filter(): + """ + Test creating a snapshot with filters for geometries and specific attributes. + Send a POST request with a Polygon payload, + including a filter for geometry type and + specific tags and attributes. + Expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ response = client.post( "/v1/snapshot/", json={ @@ -664,6 +813,14 @@ def test_snapshot_and_filter(): def test_snapshot_authentication_uuid(): + """ + Test creating a snapshot with a valid access token and 'uuid' as False. + Send a POST request with a Polygon payload + and headers including access token. + Expect a status code of 200 or 403. + If 200, wait for the task to complete. + If 403, raise an exception. + """ headers = {"access-token": access_token} payload = { "geometry": { @@ -683,13 +840,24 @@ def test_snapshot_authentication_uuid(): response = client.post("/v1/snapshot/", json=payload, headers=headers) - assert response.status_code == 200 - res = response.json() - track_link = res["track_link"] - wait_for_task_completion(track_link) + assert response.status_code in [200, 403], f"Unexpected status code: {response.status_code}" + if response.status_code == 200: + res = response.json() + track_link = res["track_link"] + wait_for_task_completion(track_link) + elif response.status_code == 403: + # Handle the 403 response accordingly + raise Exception("Access Forbidden: You may not have permission to access this resource.") def test_snapshot_bind_zip(): + """ + Test creating a snapshot with a valid access token and 'bindZip' as False. + Send a POST request with a Polygon payload, + and headers including access token and 'bindZip' parameter. + Expect a 200 status code, retrieve the 'track_link' + and wait for the task to complete. + """ headers = {"access-token": access_token} payload = { "geometry": { @@ -719,6 +887,12 @@ def test_snapshot_bind_zip(): def test_snapshot_plain(): + """ + Test creating a snapshot of a Polygon geometry. + Send a POST request with a Polygon payload and + don't include any headers or authentication. + Expect a 200 status code indicating a successful snapshot creation. + """ response = client.post( "/v1/snapshot/plain/", json={ @@ -743,6 +917,17 @@ def test_snapshot_plain(): def test_stats_endpoint_custom_polygon(): + """ + Test obtaining statistics for a custom polygon with valid access + token. + + Send a POST request to the "/v1/stats/polygon/" endpoint, + including "access-token" as a header and a Polygon payload. + + Assert that: + - the status code is 200 + - the response contains the correct indicators property + """ headers = {"access-token": access_token} payload = { "geometry": { @@ -768,8 +953,18 @@ def test_stats_endpoint_custom_polygon(): == "https://github.com/hotosm/raw-data-api/tree/develop/docs/src/stats/indicators.md" ) - def test_stats_endpoint_iso3(): + """ + Test obtaining statistics for an ISO 3166-1 3-letter code (npl) + + Send a POST request to the "/v1/stats/polygon/" endpoint, + including "access-token" as a header and an ISO 3166-1 3-letter + country code ("npl") in the request body. + + Assert that: + - the status code is 200 + - the response contains the correct indicators property + """ headers = {"access-token": access_token} payload = {"iso3": "npl"} @@ -782,11 +977,23 @@ def test_stats_endpoint_iso3(): == "https://github.com/hotosm/raw-data-api/tree/develop/docs/src/stats/indicators.md" ) - # HDX def test_hdx_submit_normal_iso3(): + """ + Test submitting an HDX custom snapshot request for ISO3 code. + + Send a POST request to the "/v1/custom/snapshot/" endpoint, + including "access-token" as a header and a custom snapshot + request in the request body. + + Assert that: + - the status code is 200 + - data is retrieved + - the track_link is assigned + - the task is completed + """ headers = {"access-token": access_token} payload = { "iso3": "NPL", @@ -814,8 +1021,21 @@ def test_hdx_submit_normal_iso3(): track_link = res["track_link"] wait_for_task_completion(track_link) - def test_hdx_submit_normal_iso3_multiple_format(): + """ + Test submitting an HDX custom snapshot request for ISO3 code + with multiple data formats. + + Send a POST request to the "/v1/custom/snapshot/" endpoint, + including "access-token" as a header and a custom snapshot + request in the request body. + + Assert that: + - the status code is 200 + - data is retrieved + - the track_link is assigned + - the task is completed + """ headers = {"access-token": access_token} payload = { "iso3": "NPL", @@ -843,8 +1063,20 @@ def test_hdx_submit_normal_iso3_multiple_format(): track_link = res["track_link"] wait_for_task_completion(track_link) - def test_hdx_submit_normal_custom_polygon(): + """ + Test submitting an HDX custom snapshot request for a custom polygon. + + Send a POST request to the "/v1/custom/snapshot/" endpoint, + including "access-token" as a header and a custom snapshot + request in the request body. + + Assert that: + - the status code is 200 + - data is retrieved + - the track_link is assigned + - the task is completed + """ headers = {"access-token": access_token} payload = { "geometry": { @@ -891,6 +1123,20 @@ def test_hdx_submit_normal_custom_polygon(): def test_custom_submit_normal_custom_polygon_TM_project(): + """ + Test uploading a custom snapshot for a Tasking Manager Project + on a custom polygon. + + Send a POST request to the "/v1/custom/snapshot/" endpoint, + including "access-token" as a header and a custom snapshot + request in the request body. + + Assert that: + - the status code is 200 + - data is retrieved + - the track_link is assigned + - the task is completed + """ headers = {"access-token": access_token} payload = { "geometry": { @@ -982,8 +1228,21 @@ def test_custom_submit_normal_custom_polygon_TM_project(): track_link = res["track_link"] wait_for_task_completion(track_link) - def test_hdx_submit_normal_custom_polygon_upload(): + """ + Test submitting an HDX custom snapshot request with custom polygon + and upload option enabled. + + Send a POST request to the "/v1/custom/snapshot/" endpoint, + including "access-token" as a header and a custom snapshot + request in the request body. + + Assert that: + - the status code is 200 + - data is retrieved + - the track_link is assigned + - the task is completed + """ headers = {"access-token": access_token} payload = { "geometry": { @@ -1029,8 +1288,21 @@ def test_hdx_submit_normal_custom_polygon_upload(): track_link = res["track_link"] wait_for_task_completion(track_link) - def test_full_hdx_set_iso(): + """ + Test uploading a full HDX dataset for an ISO 3166-1 alpha-3 code + and its geodata. + + Send a POST request to the "/v1/custom/snapshot/" endpoint, + including "access-token" as a header and a custom snapshot + request in the request body. + + Assert that: + - the status code is 200 + - data is retrieved + - the track_link is assigned + - the task is completed + """ headers = {"access-token": access_token} payload = { "iso3": "NPL", @@ -1314,10 +1586,13 @@ def test_full_hdx_set_iso(): track_link = res["track_link"] wait_for_task_completion(track_link) - # ## Tasks connection def test_worker_connection(): + """ + Tests the connection between the API and the worker. + This test sends a request to the API's ping endpoint. If the API is connected to the worker, it will return a 200 status code. + """ response = client.get("/v1/tasks/ping/") assert response.status_code == 200 diff --git a/tests/test_app.py b/tests/test_app.py index 087609f7..0b0bd530 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -22,6 +22,13 @@ def test_rawdata_current_snapshot_geometry_query(): + """ + Test the raw data current snapshot query on a Polygon geometry with specific + filters for tags and attributes, using ST_Intersects as the geometry + comparison method. + + This test covers a specific scenario with a handcrafted query result. + """ test_param = { "geometry": { "type": "Polygon", @@ -72,6 +79,13 @@ def test_rawdata_current_snapshot_geometry_query(): def test_rawdata_current_snapshot_normal_query(): + """ + Test the raw data current snapshot query on a Polygon geometry without + specific filters for tags and attributes, using ST_Intersects as the + geometry comparison method. + + This test covers a basic scenario with a handcrafted query result. + """ test_param = { "geometry": { "type": "Polygon", @@ -117,6 +131,14 @@ def test_rawdata_current_snapshot_normal_query(): def test_rawdata_current_snapshot_normal_query_ST_within(): + """ + Test the raw data current snapshot query on a Polygon geometry without + specific filters for tags and attributes, using ST_Within as the + geometry comparison method. + + This test covers a scenario similar to the other normal query tests with + ST_Within instead of ST_Intersects. + """ test_param = { "geometry": { "type": "Polygon", @@ -161,6 +183,14 @@ def test_rawdata_current_snapshot_normal_query_ST_within(): def test_attribute_filter_rawdata(): + """ + Test the raw data current snapshot query on a Polygon geometry with + specific attributes filter and tags filter with ST_Intersects and + ST_Union, and using a specific grid ID list. + + This test covers a complex scenario with multiple conditions and + a specific grid ID list. + """ test_param = { "geometry": { "type": "Polygon", @@ -212,6 +242,14 @@ def test_attribute_filter_rawdata(): def test_and_filters(): + """ + Test the raw data current snapshot query on a Polygon geometry with + complex conditions for tags filter using multiple join_and, + ST_Intersects and ST_Union. + + This test covers a specific scenario with multiple join_and clauses + for joined tags. + """ test_param = { "fileName": "Destroyed_Buildings_Turkey", "geometry": {