Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validation to Template API endpoints #4344

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api_app/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.20.4"
__version__ = "0.21.0"
2 changes: 2 additions & 0 deletions api_app/api/routes/shared_service_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ async def get_shared_service_template(shared_service_template_name: str, is_upda

@shared_service_templates_core_router.post("/shared-service-templates", status_code=status.HTTP_201_CREATED, response_model=SharedServiceTemplateInResponse, response_model_exclude_none=True, name=strings.API_CREATE_SHARED_SERVICE_TEMPLATES, dependencies=[Depends(get_current_admin_user)])
async def register_shared_service_template(template_input: SharedServiceTemplateInCreate, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> ResourceTemplateInResponse:
if template_input.resourceType != ResourceType.SharedService:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=strings.INVALID_RESOURCE_TYPE.format(ResourceType.SharedService, template_input.resourceType))
try:
return await template_repo.create_and_validate_template(template_input, ResourceType.SharedService)
except EntityVersionExist:
Expand Down
3 changes: 2 additions & 1 deletion api_app/api/routes/user_resource_templates.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import parse_obj_as
Expand Down Expand Up @@ -32,6 +31,8 @@ async def get_user_resource_template(service_template_name: str, user_resource_t

@user_resource_templates_core_router.post("/workspace-service-templates/{service_template_name}/user-resource-templates", status_code=status.HTTP_201_CREATED, response_model=UserResourceTemplateInResponse, response_model_exclude_none=True, name=strings.API_CREATE_USER_RESOURCE_TEMPLATES, dependencies=[Depends(get_current_admin_user)])
async def register_user_resource_template(template_input: UserResourceTemplateInCreate, template_repo=Depends(get_repository(ResourceTemplateRepository)), workspace_service_template=Depends(get_workspace_service_template_by_name_from_path)) -> UserResourceTemplateInResponse:
if template_input.resourceType != ResourceType.UserResource:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=strings.INVALID_RESOURCE_TYPE.format(ResourceType.UserResource, template_input.resourceType))
try:
return await template_repo.create_and_validate_template(template_input, ResourceType.UserResource, workspace_service_template.name)
except EntityVersionExist:
Expand Down
2 changes: 2 additions & 0 deletions api_app/api/routes/workspace_service_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ async def get_workspace_service_template(service_template_name: str, is_update:

@workspace_service_templates_core_router.post("/workspace-service-templates", status_code=status.HTTP_201_CREATED, response_model=WorkspaceServiceTemplateInResponse, response_model_exclude_none=True, name=strings.API_CREATE_WORKSPACE_SERVICE_TEMPLATES, dependencies=[Depends(get_current_admin_user)])
async def register_workspace_service_template(template_input: WorkspaceServiceTemplateInCreate, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> ResourceTemplateInResponse:
if template_input.resourceType != ResourceType.WorkspaceService:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=strings.INVALID_RESOURCE_TYPE.format(ResourceType.WorkspaceService, template_input.resourceType))
try:
return await template_repo.create_and_validate_template(template_input, ResourceType.WorkspaceService)
except EntityVersionExist:
Expand Down
2 changes: 2 additions & 0 deletions api_app/api/routes/workspace_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ async def get_workspace_template(workspace_template_name: str, is_update: bool =

@workspace_templates_admin_router.post("/workspace-templates", status_code=status.HTTP_201_CREATED, response_model=WorkspaceTemplateInResponse, response_model_exclude_none=True, name=strings.API_CREATE_WORKSPACE_TEMPLATES)
async def register_workspace_template(template_input: WorkspaceTemplateInCreate, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> ResourceTemplateInResponse:
if template_input.resourceType != ResourceType.Workspace:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=strings.INVALID_RESOURCE_TYPE.format(ResourceType.Workspace, template_input.resourceType))
try:
return await template_repo.create_and_validate_template(template_input, ResourceType.Workspace)
except EntityVersionExist:
Expand Down
1 change: 1 addition & 0 deletions api_app/models/schemas/resource_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
class ResourceTemplateInCreate(BaseModel):
name: str = Field(title="Template name")
version: str = Field(title="Template version")
resourceType: str = Field(title="Resource type")
current: bool = Field(title="Mark this version as current")
json_schema: Dict = Field(title="JSON Schema compliant template")
customActions: List[CustomAction] = Field(default=[], title="Custom actions")
Expand Down
3 changes: 3 additions & 0 deletions api_app/resources/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,6 @@

# Value that a sensitive is replaced with in Cosmos
REDACTED_SENSITIVE_VALUE = "REDACTED"

# Common error message for invalid resource type
INVALID_RESOURCE_TYPE = "Invalid resourceType. Expected '{}'. Received '{}'."
4 changes: 4 additions & 0 deletions api_app/tests_ma/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
def input_workspace_template():
return WorkspaceTemplateInCreate(
name="my-tre-workspace",
resourceType=ResourceType.Workspace,
version="0.0.1",
current=True,
json_schema={
Expand Down Expand Up @@ -99,6 +100,7 @@ def input_workspace_service_template():
return WorkspaceServiceTemplateInCreate(
name="my-tre-workspace-service",
version="0.0.1",
resourceType=ResourceType.WorkspaceService,
current=True,
json_schema={
"$schema": "http://json-schema.org/draft-07/schema",
Expand All @@ -119,6 +121,7 @@ def input_workspace_service_template():
def input_user_resource_template():
return UserResourceTemplateInCreate(
name="my-tre-user-resource",
resourceType=ResourceType.UserResource,
version="0.0.1",
current=True,
json_schema={
Expand All @@ -140,6 +143,7 @@ def input_user_resource_template():
def input_shared_service_template():
return SharedServiceTemplateInCreate(
name="my-tre-shared-service",
resourceType=ResourceType.SharedService,
version="0.0.1",
current=True,
json_schema={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,14 @@ async def test_creating_a_shared_service_template_raises_http_422_if_step_ids_ar
response = await client.post(app.url_path_for(strings.API_CREATE_SHARED_SERVICE_TEMPLATES), json=input_shared_service_template.dict())

assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

# POST /shared-service-templates
async def test_post_shared_service_template_with_invalid_resource_type(self, app, client, input_shared_service_template):

input_data = input_shared_service_template.dict()
input_data["resourceType"] = ResourceType.WorkspaceService

response = await client.post(app.url_path_for(strings.API_CREATE_SHARED_SERVICE_TEMPLATES), json=input_data)

assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
assert response.text == strings.INVALID_RESOURCE_TYPE.format(ResourceType.SharedService, input_data["resourceType"])
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,16 @@ async def test_creating_a_user_resource_template_raises_http_422_if_step_ids_are

assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

@patch("api.dependencies.workspace_service_templates.ResourceTemplateRepository.get_current_template")
async def test_post_user_resource_template_with_invalid_resource_type(self, _, app, client, input_user_resource_template):
input_data = input_user_resource_template.dict()
input_data["resourceType"] = ResourceType.WorkspaceService

response = await client.post(app.url_path_for(strings.API_CREATE_USER_RESOURCE_TEMPLATES, service_template_name="guacamole"), json=input_data)

assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
assert response.text == strings.INVALID_RESOURCE_TYPE.format(ResourceType.UserResource, input_data["resourceType"])


class TestUserResourceTemplatesNotRequiringAdminRights:
@pytest.fixture(autouse=True, scope='class')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ async def test_get_workspace_service_templates_returns_template_names_and_descri
@patch("api.routes.workspace_service_templates.ResourceTemplateRepository.create_template")
@patch("api.routes.workspace_service_templates.ResourceTemplateRepository.get_current_template")
@patch("api.routes.workspace_service_templates.ResourceTemplateRepository.get_template_by_name_and_version")
async def test_when_updating_current_and_service_template_not_found_create_one(self, get_name_ver_mock, get_current_mock, create_template_mock, app, client, input_workspace_template, basic_workspace_service_template):
async def test_when_updating_current_and_service_template_not_found_create_one(self, get_name_ver_mock, get_current_mock, create_template_mock, app, client, input_workspace_service_template, basic_workspace_service_template):
get_name_ver_mock.side_effect = EntityDoesNotExist
get_current_mock.side_effect = EntityDoesNotExist
create_template_mock.return_value = basic_workspace_service_template

response = await client.post(app.url_path_for(strings.API_CREATE_WORKSPACE_SERVICE_TEMPLATES), json=input_workspace_template.dict())
response = await client.post(app.url_path_for(strings.API_CREATE_WORKSPACE_SERVICE_TEMPLATES), json=input_workspace_service_template.dict())

assert response.status_code == status.HTTP_201_CREATED

Expand All @@ -98,29 +98,29 @@ async def test_when_updating_current_and_service_template_not_found_create_one(s
@patch("api.routes.workspace_service_templates.ResourceTemplateRepository.update_item")
@patch("api.routes.workspace_service_templates.ResourceTemplateRepository.get_current_template")
@patch("api.routes.workspace_service_templates.ResourceTemplateRepository.get_template_by_name_and_version")
async def test_when_updating_current_and_service_template_found_update_and_add(self, get_template_by_name_and_version_mock, get_current_template_mock, update_item_mock, create_template_mock, app, client, input_workspace_template, basic_workspace_service_template):
async def test_when_updating_current_and_service_template_found_update_and_add(self, get_template_by_name_and_version_mock, get_current_template_mock, update_item_mock, create_template_mock, app, client, input_workspace_service_template, basic_workspace_service_template):
get_template_by_name_and_version_mock.side_effect = EntityDoesNotExist

get_current_template_mock.return_value = basic_workspace_service_template
create_template_mock.return_value = basic_workspace_service_template

response = await client.post(app.url_path_for(strings.API_CREATE_WORKSPACE_SERVICE_TEMPLATES), json=input_workspace_template.dict())
response = await client.post(app.url_path_for(strings.API_CREATE_WORKSPACE_SERVICE_TEMPLATES), json=input_workspace_service_template.dict())

updated_current_workspace_template = basic_workspace_service_template
updated_current_workspace_template.current = False
update_item_mock.assert_called_once_with(updated_current_workspace_template.dict())
updated_current_workspace_service_template = basic_workspace_service_template
updated_current_workspace_service_template.current = False
update_item_mock.assert_called_once_with(updated_current_workspace_service_template.dict())
assert response.status_code == status.HTTP_201_CREATED

# POST /workspace-service-templates/
@patch("api.routes.workspace_service_templates.ResourceTemplateRepository.create_template")
@patch("api.routes.workspace_service_templates.ResourceTemplateRepository.get_current_template")
@patch("api.routes.workspace_service_templates.ResourceTemplateRepository.get_template_by_name_and_version")
async def test_when_creating_service_template_enriched_service_template_is_returned(self, get_template_by_name_and_version_mock, get_current_template_mock, create_template_mock, app, client, input_workspace_template, basic_workspace_service_template):
async def test_when_creating_service_template_enriched_service_template_is_returned(self, get_template_by_name_and_version_mock, get_current_template_mock, create_template_mock, app, client, input_workspace_service_template, basic_workspace_service_template):
get_template_by_name_and_version_mock.side_effect = EntityDoesNotExist
get_current_template_mock.side_effect = EntityDoesNotExist
create_template_mock.return_value = basic_workspace_service_template

response = await client.post(app.url_path_for(strings.API_CREATE_WORKSPACE_SERVICE_TEMPLATES), json=input_workspace_template.dict())
response = await client.post(app.url_path_for(strings.API_CREATE_WORKSPACE_SERVICE_TEMPLATES), json=input_workspace_service_template.dict())

expected_template = parse_obj_as(WorkspaceTemplateInResponse, enrich_workspace_service_template(basic_workspace_service_template))
assert json.loads(response.text)["required"] == expected_template.dict(exclude_unset=True)["required"]
Expand Down Expand Up @@ -176,3 +176,12 @@ async def test_workspace_service_templates_by_name_returns_error_status_based_on
response = await client.get(app.url_path_for(strings.API_GET_WORKSPACE_SERVICE_TEMPLATE_BY_NAME, service_template_name="template1"))

assert response.status_code == expected_status

# POST /workspace-service-templates/
async def test_post_workspace_service_template_with_invalid_resource_type(self, app, client, input_workspace_service_template):
input_data = input_workspace_service_template.dict()
input_data['resourceType'] = ResourceType.UserResource
response = await client.post(app.url_path_for(strings.API_CREATE_WORKSPACE_SERVICE_TEMPLATES), json=input_data)

assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
assert response.text == strings.INVALID_RESOURCE_TYPE.format(ResourceType.WorkspaceService, input_data["resourceType"])
14 changes: 12 additions & 2 deletions api_app/tests_ma/test_api/test_routes/test_workspace_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,16 @@ async def test_when_creating_workspace_service_template_service_resource_type_is
get_current_template_mock.side_effect = EntityDoesNotExist
create_template_mock.return_value = basic_workspace_service_template

await client.post(app.url_path_for(strings.API_CREATE_WORKSPACE_SERVICE_TEMPLATES), json=input_workspace_template.dict())
await client.post(app.url_path_for(strings.API_CREATE_WORKSPACE_TEMPLATES), json=input_workspace_template.dict())

create_template_mock.assert_called_once_with(input_workspace_template, ResourceType.Workspace, '')

# POST /workspace-templates
async def test_post_workspace_template_with_invalid_resource_type(self, app, client, input_workspace_template):
input_data = input_workspace_template.dict()
input_data["resourceType"] = ResourceType.WorkspaceService

response = await client.post(app.url_path_for(strings.API_CREATE_WORKSPACE_TEMPLATES), json=input_data)

create_template_mock.assert_called_once_with(input_workspace_template, ResourceType.WorkspaceService, '')
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
assert response.text == strings.INVALID_RESOURCE_TYPE.format(ResourceType.Workspace, input_data["resourceType"])
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ async def test_create_workspace_template_succeeds_without_required(uuid_mock, sa
expected_type = ResourceType.Workspace
input_workspace_template = WorkspaceTemplateInCreate(
name="my-tre-workspace",
resourceType=ResourceType.Workspace,
version="0.0.1",
current=True,
json_schema={
Expand Down
Loading