diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/httpx_calls_capture_parameters.py b/packages/pytest-simcore/src/pytest_simcore/helpers/httpx_calls_capture_parameters.py index 3e44b62c3b86..a58544a59f09 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/httpx_calls_capture_parameters.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/httpx_calls_capture_parameters.py @@ -7,7 +7,9 @@ class CapturedParameterSchema(BaseModel): title: str | None = None - type_: Literal["str", "int", "float", "bool"] | None = Field(None, alias="type") + type_: Literal["str", "int", "float", "bool", "null"] | None = Field( + None, alias="type" + ) pattern: str | None = None format_: Literal["uuid"] | None = Field(None, alias="format") exclusiveMinimum: bool | None = None diff --git a/services/api-server/tests/mocks/get_program_release_not_found.json b/services/api-server/tests/mocks/get_program_release_not_found.json new file mode 100644 index 000000000000..72a58825d422 --- /dev/null +++ b/services/api-server/tests/mocks/get_program_release_not_found.json @@ -0,0 +1,44 @@ +[ + { + "name": "GET /services/simcore%2Fservices%2Fdynamic%2Fmy_program/1.0.0", + "description": "", + "method": "GET", + "host": "catalog", + "path": { + "path": "/v0/services/{service_key}/{service_version}", + "path_parameters": [ + { + "in": "path", + "name": "service_key", + "required": true, + "schema": { + "title": "Service Key", + "type": "str", + "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$" + }, + "response_value": "services" + }, + { + "in": "path", + "name": "service_version", + "required": true, + "schema": { + "title": "Service Version", + "type": "str", + "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$" + }, + "response_value": "simcore/services/dynamic/my_program" + } + ] + }, + "query": "user_id=1", + "response_body": { + "error": { + "errors": [ + "Not Found" + ] + } + }, + "status_code": 404 + } +] diff --git a/services/api-server/tests/mocks/get_program_release_success.json b/services/api-server/tests/mocks/get_program_release_success.json new file mode 100644 index 000000000000..4a35e7dc2dae --- /dev/null +++ b/services/api-server/tests/mocks/get_program_release_success.json @@ -0,0 +1,83 @@ +[ + { + "name": "GET /services/simcore%2Fservices%2Fdynamic%2Felectrode-selector/2.1.3", + "description": "", + "method": "GET", + "host": "catalog", + "path": { + "path": "/v0/services/{service_key}/{service_version}", + "path_parameters": [ + { + "in": "path", + "name": "service_version", + "required": true, + "schema": { + "title": "Service Version", + "type": "str", + "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$" + }, + "response_value": "simcore/services/dynamic/electrode-selector" + }, + { + "in": "path", + "name": "service_key", + "required": true, + "schema": { + "title": "Service Key", + "type": "str", + "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$" + }, + "response_value": "services" + } + ] + }, + "query": "user_id=1", + "response_body": { + "name": "electrode-selector", + "thumbnail": null, + "icon": null, + "description": "GUI for selecting/visualizing electrode groups and target tissue", + "description_ui": false, + "version_display": null, + "deprecated": null, + "classifiers": [], + "quality": {}, + "key": "simcore/services/dynamic/electrode-selector", + "version": "2.1.3", + "integration-version": "1.0.0", + "type": "dynamic", + "authors": [ + { + "name": "Odei Maiz", + "email": "maiz@itis.swiss", + "affiliation": "ITIS Foundation" + } + ], + "contact": "maiz@itis.swiss", + "inputs": { + "input_1": { + "displayOrder": 1.0, + "label": "Sim info", + "description": "Information on which simulations are done", + "type": "data:application/json" + }, + "input_2": { + "displayOrder": 2.0, + "label": "Targets list", + "description": "List of available targets for the model", + "type": "data:application/json" + } + }, + "outputs": { + "output_1": { + "displayOrder": 1.0, + "label": "Output", + "description": "Electrode groups and Target tissue", + "type": "data:application/json" + } + }, + "image_digest": "sha256:37f2603d079cfedcc77cce6c0846eba6668c52a4da15d81ef4e9a742c081b224", + "owner": null + } + } +] diff --git a/services/api-server/tests/mocks/list_programs_success.json b/services/api-server/tests/mocks/list_programs_success.json new file mode 100644 index 000000000000..66434afa5b97 --- /dev/null +++ b/services/api-server/tests/mocks/list_programs_success.json @@ -0,0 +1,92 @@ +[ + { + "name": "GET /services", + "description": "", + "method": "GET", + "host": "catalog", + "path": { + "path": "/v0/services", + "path_parameters": [] + }, + "query": "user_id=1&details=true", + "response_body": [ + { + "name": "File Picker", + "thumbnail": null, + "icon": null, + "description": "File Picker", + "description_ui": false, + "version_display": null, + "deprecated": null, + "classifiers": [], + "quality": {}, + "accessRights": { + "1": { + "execute_access": true, + "write_access": false + } + }, + "key": "simcore/services/frontend/file-picker", + "version": "1.0.0", + "integration-version": "1.0.0", + "type": "frontend", + "authors": [ + { + "name": "Unknown", + "email": "unknown@osparc.io", + "affiliation": "unknown" + } + ], + "contact": "unknown@osparc.io", + "inputs": {}, + "outputs": { + "outFile": { + "displayOrder": 0.0, + "label": "File", + "description": "Chosen File", + "type": "data:*/*" + } + }, + "owner": null + }, + { + "name": "Number parameter", + "thumbnail": "https://fakeimg.pl/100x100/ff0000%2C128/000%2C255/?text=number", + "icon": null, + "description": "Produces a number value at its outputs", + "description_ui": false, + "version_display": null, + "deprecated": null, + "classifiers": [], + "quality": {}, + "accessRights": { + "1": { + "execute_access": true, + "write_access": false + } + }, + "key": "simcore/services/frontend/parameter/number", + "version": "1.0.0", + "integration-version": "1.0.0", + "type": "frontend", + "authors": [ + { + "name": "Unknown", + "email": "unknown@osparc.io", + "affiliation": "unknown" + } + ], + "contact": "unknown@osparc.io", + "inputs": {}, + "outputs": { + "out_1": { + "label": "number_source", + "description": "Input number value", + "type": "number" + } + }, + "owner": null + } + ] + } +] diff --git a/services/api-server/tests/unit/conftest.py b/services/api-server/tests/unit/conftest.py index 8fb9fb2a4452..6074685c92e4 100644 --- a/services/api-server/tests/unit/conftest.py +++ b/services/api-server/tests/unit/conftest.py @@ -499,7 +499,7 @@ def _patch(href): return data.status_href, data.result_href return mocker.patch( - "simcore_service_api_server.services.webserver._get_lrt_urls", + "simcore_service_api_server.services_http.webserver._get_lrt_urls", side_effect=_get_lrt_urls, ) diff --git a/services/api-server/tests/unit/test_api_programs.py b/services/api-server/tests/unit/test_api_programs.py new file mode 100644 index 000000000000..248962f72d58 --- /dev/null +++ b/services/api-server/tests/unit/test_api_programs.py @@ -0,0 +1,109 @@ +from pathlib import Path + +import httpx +import pytest +from fastapi import status +from httpx import AsyncClient +from pytest_simcore.helpers.httpx_calls_capture_models import CreateRespxMockCallback +from simcore_service_api_server._meta import API_VTAG + + +@pytest.mark.parametrize( + "capture,expected_status_code", + [ + ("create_program_job_invalid_program.json", status.HTTP_404_NOT_FOUND), + ("create_program_job_success.json", status.HTTP_201_CREATED), + ], +) +async def test_create_program_job( + client: AsyncClient, + mocked_webserver_rest_api_base, + create_respx_mock_from_capture: CreateRespxMockCallback, + auth: httpx.BasicAuth, + project_tests_dir: Path, + capture: str, + expected_status_code: int, +): + respx_mock = create_respx_mock_from_capture( + respx_mocks=[mocked_webserver_rest_api_base], + capture_path=project_tests_dir / "mocks" / capture, + side_effects_callbacks=[], + ) + assert respx_mock + + program_key = "simcore/services/comp/my_program" + version = "1.0.0" + + response = await client.post( + f"{API_VTAG}/programs/{program_key}/releases/{version}/jobs", + auth=auth, + ) + assert response.status_code == expected_status_code + if response.status_code == status.HTTP_201_CREATED: + assert "job_id" in response.json() + + +@pytest.mark.parametrize( + "capture,expected_status_code", + [ + ("list_programs_success.json", status.HTTP_200_OK), + ], +) +async def test_list_programs( + client: AsyncClient, + mocked_catalog_rest_api_base, + create_respx_mock_from_capture: CreateRespxMockCallback, + auth: httpx.BasicAuth, + project_tests_dir: Path, + capture: str, + expected_status_code: int, +): + respx_mock = create_respx_mock_from_capture( + respx_mocks=[mocked_catalog_rest_api_base], + capture_path=project_tests_dir / "mocks" / capture, + side_effects_callbacks=[], + ) + assert respx_mock + + response = await client.get(f"{API_VTAG}/programs", auth=auth) + assert response.status_code == expected_status_code + if response.status_code == status.HTTP_200_OK: + programs = response.json() + assert isinstance(programs, list) + + +@pytest.mark.parametrize( + "capture,expected_status_code", + [ + ("get_program_release_success.json", status.HTTP_200_OK), + ("get_program_release_not_found.json", status.HTTP_404_NOT_FOUND), + ], +) +async def test_get_program_release( + client: AsyncClient, + mocked_catalog_rest_api_base, + create_respx_mock_from_capture: CreateRespxMockCallback, + auth: httpx.BasicAuth, + project_tests_dir: Path, + capture: str, + expected_status_code: int, +): + respx_mock = create_respx_mock_from_capture( + respx_mocks=[mocked_catalog_rest_api_base], + capture_path=project_tests_dir / "mocks" / capture, + side_effects_callbacks=[], + ) + assert respx_mock + + program_key = "simcore/services/dynamic/electrode-selector" + version = "2.1.3" + + response = await client.get( + f"{API_VTAG}/programs/{program_key}/releases/{version}", auth=auth + ) + assert response.status_code == expected_status_code + if response.status_code == status.HTTP_200_OK: + program_release = response.json() + assert "id" in program_release + assert program_release["id"] == program_key + assert program_release["version"] == version