diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f6cf93c58..1775976959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ENHANCEMENTS: * Streamline Azure bootstrap by replacing legacy dependency checks with `bootstrap_azure_env.sh` ([#2993](https://github.com/microsoft/AzureTRE/issues/2993)) * Create Azure provider registration script ([#2993](https://github.com/microsoft/AzureTRE/issues/4653)) * Update oauth2-proxy and Tomcat versions to latest in Guacamole container ([#4688](https://github.com/microsoft/AzureTRE/pull/4688)]) +* Standardize database query parameter handling across repository classes ([#4697](https://github.com/microsoft/AzureTRE/issues/4697)) BUG FIXES: * Fix disable public network access for stwebcertsTREID is still flagging in Defender ([#4640](https://github.com/microsoft/AzureTRE/issues/4640)) diff --git a/api_app/_version.py b/api_app/_version.py index 7d98e1ed73..6fea77c010 100644 --- a/api_app/_version.py +++ b/api_app/_version.py @@ -1 +1 @@ -__version__ = "0.24.10" +__version__ = "0.25.0" diff --git a/api_app/api/routes/workspaces.py b/api_app/api/routes/workspaces.py index e0cdc9eff7..a2dcbe52fa 100644 --- a/api_app/api/routes/workspaces.py +++ b/api_app/api/routes/workspaces.py @@ -1,6 +1,7 @@ import asyncio -from fastapi import APIRouter, Depends, HTTPException, Header, status, Request, Response +from fastapi import APIRouter, Depends, HTTPException, Header, Path, status, Request, Response +from pydantic import UUID4 from jsonschema.exceptions import ValidationError @@ -362,8 +363,8 @@ async def retrieve_workspace_service_history_by_workspace_service_id(workspace_s # USER RESOURCE ROUTES @user_resources_workspace_router.get("/workspaces/{workspace_id}/workspace-services/{service_id}/user-resources", response_model=UserResourcesInList, name=strings.API_GET_MY_USER_RESOURCES, dependencies=[Depends(get_workspace_by_id_from_path)]) async def retrieve_user_resources_for_workspace_service( - workspace_id: str, - service_id: str, + workspace_id: UUID4 = Path(...), + service_id: UUID4 = Path(...), user=Depends(get_current_workspace_owner_or_researcher_user_or_airlock_manager), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), user_resource_repo=Depends(get_repository(UserResourceRepository))) -> UserResourcesInList: diff --git a/api_app/db/repositories/resource_templates.py b/api_app/db/repositories/resource_templates.py index 66674e7cf0..655f494684 100644 --- a/api_app/db/repositories/resource_templates.py +++ b/api_app/db/repositories/resource_templates.py @@ -22,7 +22,12 @@ async def create(cls): @staticmethod def _template_by_name_query(name: str, resource_type: ResourceType) -> str: - return f'SELECT * FROM c WHERE c.resourceType = "{resource_type}" AND c.name = "{name}"' + query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.name = @name' + parameters = [ + {'name': '@resourceType', 'value': resource_type}, + {'name': '@name', 'value': name} + ] + return query, parameters @staticmethod def enrich_template(template: ResourceTemplate, is_update: bool = False) -> dict: @@ -42,10 +47,14 @@ async def get_templates_information(self, resource_type: ResourceType, user_role :param user_roles: If set, only return templates that the user is authorized to use. template.authorizedRoles should contain at least one of user_roles """ - query = f'SELECT c.name, c.title, c.description, c.authorizedRoles FROM c WHERE c.resourceType = "{resource_type}" AND c.current = true' + query = 'SELECT c.name, c.title, c.description, c.authorizedRoles FROM c WHERE c.resourceType = @resourceType AND c.current = true' + parameters = [ + {'name': '@resourceType', 'value': resource_type} + ] if resource_type == ResourceType.UserResource: - query += f' AND c.parentWorkspaceService = "{parent_service_name}"' - template_infos = await self.query(query=query) + query += ' AND c.parentWorkspaceService = @parentWorkspaceService' + parameters.append({'name': '@parentWorkspaceService', 'value': parent_service_name}) + template_infos = await self.query(query=query, parameters=parameters) templates = [parse_obj_as(ResourceTemplateInformation, info) for info in template_infos] if not user_roles: @@ -57,10 +66,12 @@ async def get_current_template(self, template_name: str, resource_type: Resource """ Returns full template for the current version of the 'template_name' template """ - query = self._template_by_name_query(template_name, resource_type) + ' AND c.current = true' + query, parameters = self._template_by_name_query(template_name, resource_type) + query += ' AND c.current = true' if resource_type == ResourceType.UserResource: - query += f' AND c.parentWorkspaceService = "{parent_service_name}"' - templates = await self.query(query=query) + query += ' AND c.parentWorkspaceService = @parentWorkspaceService' + parameters.append({'name': '@parentWorkspaceService', 'value': parent_service_name}) + templates = await self.query(query=query, parameters=parameters) if len(templates) == 0: raise EntityDoesNotExist if len(templates) > 1: @@ -76,17 +87,20 @@ async def get_template_by_name_and_version(self, name: str, version: str, resour For UserResource templates, you also need to pass in 'parent_service_name' as a parameter """ - query = self._template_by_name_query(name, resource_type) + f' AND c.version = "{version}"' + query, parameters = self._template_by_name_query(name, resource_type) + query += ' AND c.version = @version' + parameters.append({'name': '@version', 'value': version}) # If querying for a user resource, we also need to add the parentWorkspaceService (name) to the query if resource_type == ResourceType.UserResource: if parent_service_name: - query += f' AND c.parentWorkspaceService = "{parent_service_name}"' + query += ' AND c.parentWorkspaceService = @parentWorkspaceService' + parameters.append({'name': '@parentWorkspaceService', 'value': parent_service_name}) else: raise Exception("When getting a UserResource template, you must pass in a 'parent_service_name'") # Execute the query and handle results - templates = await self.query(query=query) + templates = await self.query(query=query, parameters=parameters) if len(templates) != 1: raise EntityDoesNotExist if resource_type == ResourceType.UserResource: diff --git a/api_app/db/repositories/resources.py b/api_app/db/repositories/resources.py index 0459698815..0ebfc6bc46 100644 --- a/api_app/db/repositories/resources.py +++ b/api_app/db/repositories/resources.py @@ -30,16 +30,21 @@ async def create(cls): await super().create(config.STATE_STORE_RESOURCES_CONTAINER) return cls - @staticmethod - def _active_resources_query(): - # get active docs (not deleted) - return f'SELECT * FROM c WHERE {IS_NOT_DELETED_CLAUSE}' - def _active_resources_by_type_query(self, resource_type: ResourceType): - return self._active_resources_query() + f' AND c.resourceType = "{resource_type}"' + query = 'SELECT * FROM c WHERE c.deploymentStatus != @deletedStatus AND c.resourceType = @resourceType' + parameters = [ + {'name': '@deletedStatus', 'value': Status.Deleted}, + {'name': '@resourceType', 'value': resource_type} + ] + return query, parameters def _active_resources_by_id_query(self, resource_id: str): - return self._active_resources_query() + f' AND c.id = "{resource_id}"' + query = 'SELECT * FROM c WHERE c.deploymentStatus != @deletedStatus AND c.id = @resourceId' + parameters = [ + {'name': '@deletedStatus', 'value': Status.Deleted}, + {'name': '@resourceId', 'value': resource_id} + ] + return query, parameters @staticmethod def _validate_resource_parameters(resource_input, resource_template): @@ -76,8 +81,13 @@ async def get_resource_by_id(self, resource_id: UUID4) -> Resource: return parse_obj_as(Resource, resource) async def get_active_resource_by_template_name(self, template_name: str) -> Resource: - query = f"SELECT TOP 1 * FROM c WHERE c.templateName = '{template_name}' AND {IS_ACTIVE_RESOURCE}" - resources = await self.query(query=query) + query = "SELECT TOP 1 * FROM c WHERE c.templateName = @templateName AND c.deploymentStatus != @deletedStatus AND c.deploymentStatus != @failedStatus" + parameters = [ + {'name': '@templateName', 'value': template_name}, + {'name': '@deletedStatus', 'value': Status.Deleted}, + {'name': '@failedStatus', 'value': Status.DeploymentFailed} + ] + resources = await self.query(query=query, parameters=parameters) if not resources: raise EntityDoesNotExist return parse_obj_as(Resource, resources[0]) @@ -130,8 +140,12 @@ async def get_resource_dependency_list(self, resource: Resource) -> List: dependent_resources_list = [] # Get all related resources - related_resources_query = f"SELECT * FROM c WHERE CONTAINS(c.resourcePath, '{parent_resource_path}') AND c.deploymentStatus != '{Status.Deleted}'" - related_resources = await self.query(query=related_resources_query) + related_resources_query = "SELECT * FROM c WHERE CONTAINS(c.resourcePath, @resourcePath) AND c.deploymentStatus != @deletedStatus" + parameters = [ + {'name': '@resourcePath', 'value': parent_resource_path}, + {'name': '@deletedStatus', 'value': Status.Deleted} + ] + related_resources = await self.query(query=related_resources_query, parameters=parameters) for resource in related_resources: resource_path = resource["resourcePath"] resource_level = resource_path.count("/") @@ -185,8 +199,3 @@ def validate_patch(self, resource_patch: ResourcePatch, resource_template_repo: def get_timestamp(self) -> float: return datetime.utcnow().timestamp() - - -# Cosmos query consts -IS_NOT_DELETED_CLAUSE = f'c.deploymentStatus != "{Status.Deleted}"' -IS_ACTIVE_RESOURCE = f'c.deploymentStatus != "{Status.Deleted}" and c.deploymentStatus != "{Status.DeploymentFailed}"' diff --git a/api_app/db/repositories/resources_history.py b/api_app/db/repositories/resources_history.py index 2ccfc061e5..306f5255a1 100644 --- a/api_app/db/repositories/resources_history.py +++ b/api_app/db/repositories/resources_history.py @@ -26,13 +26,17 @@ def is_valid_uuid(resourceId): def resource_history_query(self, resourceId: str): logger.debug("Validate sanity of resourceId") self.is_valid_uuid(resourceId) - return f'SELECT * FROM c WHERE c.resourceId = "{resourceId}"' + query = 'SELECT * FROM c WHERE c.resourceId = @resourceId' + parameters = [ + {'name': '@resourceId', 'value': resourceId} + ] + return query, parameters async def get_resource_history_by_resource_id(self, resource_id: str) -> List[ResourceHistoryItem]: - query = self.resource_history_query(resource_id) + query, parameters = self.resource_history_query(resource_id) try: logger.info(f"Fetching history for resource {resource_id}") - resource_history_items = await self.query(query=query) + resource_history_items = await self.query(query=query, parameters=parameters) logger.debug(f"Got {len(resource_history_items)} history items for resource {resource_id}") except EntityDoesNotExist: logger.info(f"No history for resource {resource_id}") diff --git a/api_app/db/repositories/shared_services.py b/api_app/db/repositories/shared_services.py index 9b8b1963f0..ceaa48d567 100644 --- a/api_app/db/repositories/shared_services.py +++ b/api_app/db/repositories/shared_services.py @@ -8,12 +8,13 @@ from models.domain.authentication import User from db.repositories.resource_templates import ResourceTemplateRepository from db.repositories.resources_history import ResourceHistoryRepository -from db.repositories.resources import ResourceRepository, IS_NOT_DELETED_CLAUSE, IS_ACTIVE_RESOURCE +from db.repositories.resources import ResourceRepository from db.errors import DuplicateEntity, EntityDoesNotExist from models.domain.shared_service import SharedService from models.schemas.resource import ResourcePatch from models.schemas.shared_service_template import SharedServiceTemplateInCreate from models.domain.resource import ResourceType +from models.domain.operation import Status class SharedServiceRepository(ResourceRepository): @@ -25,18 +26,36 @@ async def create(cls): @staticmethod def shared_service_query(shared_service_id: str): - return f'SELECT * FROM c WHERE c.resourceType = "{ResourceType.SharedService}" AND c.id = "{shared_service_id}"' + query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.id = @sharedServiceId' + parameters = [ + {'name': '@resourceType', 'value': ResourceType.SharedService}, + {'name': '@sharedServiceId', 'value': shared_service_id} + ] + return query, parameters @staticmethod def active_shared_services_query(): - return f'SELECT * FROM c WHERE {IS_NOT_DELETED_CLAUSE} AND c.resourceType = "{ResourceType.SharedService}"' + query = 'SELECT * FROM c WHERE c.deploymentStatus != @deletedStatus AND c.resourceType = @resourceType' + parameters = [ + {'name': '@deletedStatus', 'value': Status.Deleted}, + {'name': '@resourceType', 'value': ResourceType.SharedService} + ] + return query, parameters @staticmethod def active_shared_service_with_template_name_query(template_name: str): - return f'SELECT * FROM c WHERE {IS_ACTIVE_RESOURCE} AND c.resourceType = "{ResourceType.SharedService}" AND c.templateName = "{template_name}"' + query = 'SELECT * FROM c WHERE c.deploymentStatus != @deletedStatus AND c.deploymentStatus != @failedStatus AND c.resourceType = @resourceType AND c.templateName = @templateName' + parameters = [ + {'name': '@deletedStatus', 'value': Status.Deleted}, + {'name': '@failedStatus', 'value': Status.DeploymentFailed}, + {'name': '@resourceType', 'value': ResourceType.SharedService}, + {'name': '@templateName', 'value': template_name} + ] + return query, parameters async def get_shared_service_by_id(self, shared_service_id: str): - shared_services = await self.query(self.shared_service_query(shared_service_id)) + query, parameters = self.shared_service_query(str(shared_service_id)) + shared_services = await self.query(query=query, parameters=parameters) if not shared_services: raise EntityDoesNotExist return parse_obj_as(SharedService, shared_services[0]) @@ -45,8 +64,8 @@ async def get_active_shared_services(self) -> List[SharedService]: """ returns list of "non-deleted" shared services linked to this shared """ - query = SharedServiceRepository.active_shared_services_query() - shared_services = await self.query(query=query) + query, parameters = SharedServiceRepository.active_shared_services_query() + shared_services = await self.query(query=query, parameters=parameters) return parse_obj_as(List[SharedService], shared_services) def get_shared_service_spec_params(self): @@ -55,7 +74,8 @@ def get_shared_service_spec_params(self): async def create_shared_service_item(self, shared_service_input: SharedServiceTemplateInCreate, user_roles: List[str]) -> Tuple[SharedService, ResourceTemplate]: shared_service_id = str(uuid.uuid4()) - existing_shared_services = await self.query(self.active_shared_service_with_template_name_query(shared_service_input.templateName)) + query, parameters = self.active_shared_service_with_template_name_query(shared_service_input.templateName) + existing_shared_services = await self.query(query=query, parameters=parameters) # Duplicate is same template (=id), same version and deployed if existing_shared_services: diff --git a/api_app/db/repositories/user_resources.py b/api_app/db/repositories/user_resources.py index 7dd49dda67..3528d28d03 100644 --- a/api_app/db/repositories/user_resources.py +++ b/api_app/db/repositories/user_resources.py @@ -9,7 +9,8 @@ import resources.strings as strings from db.errors import EntityDoesNotExist from db.repositories.resource_templates import ResourceTemplateRepository -from db.repositories.resources import ResourceRepository, IS_NOT_DELETED_CLAUSE +from db.repositories.resources import ResourceRepository +from models.domain.operation import Status from models.domain.resource import ResourceType from models.domain.user_resource import UserResource from models.schemas.resource import ResourcePatch @@ -25,11 +26,24 @@ async def create(cls): @staticmethod def user_resources_query(workspace_id: str, service_id: str): - return f'SELECT * FROM c WHERE c.resourceType = "{ResourceType.UserResource}" AND c.parentWorkspaceServiceId = "{service_id}" AND c.workspaceId = "{workspace_id}"' + query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.parentWorkspaceServiceId = @serviceId AND c.workspaceId = @workspaceId' + parameters = [ + {'name': '@resourceType', 'value': ResourceType.UserResource}, + {'name': '@serviceId', 'value': service_id}, + {'name': '@workspaceId', 'value': workspace_id} + ] + return query, parameters @staticmethod def active_user_resources_query(workspace_id: str, service_id: str): - return f'SELECT * FROM c WHERE {IS_NOT_DELETED_CLAUSE} AND c.resourceType = "{ResourceType.UserResource}" AND c.parentWorkspaceServiceId = "{service_id}" AND c.workspaceId = "{workspace_id}"' + query = 'SELECT * FROM c WHERE c.deploymentStatus != @deletedStatus AND c.resourceType = @resourceType AND c.parentWorkspaceServiceId = @serviceId AND c.workspaceId = @workspaceId' + parameters = [ + {'name': '@deletedStatus', 'value': Status.Deleted}, + {'name': '@resourceType', 'value': ResourceType.UserResource}, + {'name': '@serviceId', 'value': service_id}, + {'name': '@workspaceId', 'value': workspace_id} + ] + return query, parameters async def create_user_resource_item(self, user_resource_input: UserResourceInCreate, workspace_id: str, parent_workspace_service_id: str, parent_template_name: str, user_id: str, user_roles: List[str], owner_id: str = None) -> Tuple[UserResource, ResourceTemplate]: full_user_resource_id = str(uuid.uuid4()) @@ -57,13 +71,15 @@ async def get_user_resources_for_workspace_service(self, workspace_id: str, serv """ returns a list of "non-deleted" user resources linked to this workspace service """ - query = self.active_user_resources_query(workspace_id, service_id) - user_resources = await self.query(query=query) + query, parameters = self.active_user_resources_query(str(workspace_id), str(service_id)) + user_resources = await self.query(query=query, parameters=parameters) return parse_obj_as(List[UserResource], user_resources) async def get_user_resource_by_id(self, workspace_id: str, service_id: str, resource_id: str) -> UserResource: - query = self.user_resources_query(workspace_id, service_id) + f' AND c.id = "{resource_id}"' - user_resources = await self.query(query=query) + query, parameters = self.user_resources_query(str(workspace_id), str(service_id)) + query += ' AND c.id = @resourceId' + parameters.append({'name': '@resourceId', 'value': str(resource_id)}) + user_resources = await self.query(query=query, parameters=parameters) if not user_resources: raise EntityDoesNotExist return parse_obj_as(UserResource, user_resources[0]) diff --git a/api_app/db/repositories/workspace_services.py b/api_app/db/repositories/workspace_services.py index 218699d307..ef21bfe6eb 100644 --- a/api_app/db/repositories/workspace_services.py +++ b/api_app/db/repositories/workspace_services.py @@ -8,7 +8,8 @@ from db.repositories.resource_templates import ResourceTemplateRepository import resources.strings as strings -from db.repositories.resources import ResourceRepository, IS_NOT_DELETED_CLAUSE +from db.repositories.resources import ResourceRepository +from models.domain.operation import Status from db.repositories.operations import OperationRepository from models.domain.workspace_service import WorkspaceService from models.schemas.resource import ResourcePatch @@ -26,18 +27,29 @@ async def create(cls): @staticmethod def workspace_services_query(workspace_id: str): - return f'SELECT * FROM c WHERE c.resourceType = "{ResourceType.WorkspaceService}" AND c.workspaceId = "{workspace_id}"' + query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.workspaceId = @workspaceId' + parameters = [ + {'name': '@resourceType', 'value': ResourceType.WorkspaceService}, + {'name': '@workspaceId', 'value': workspace_id} + ] + return query, parameters @staticmethod def active_workspace_services_query(workspace_id: str): - return f'SELECT * FROM c WHERE {IS_NOT_DELETED_CLAUSE} AND c.resourceType = "{ResourceType.WorkspaceService}" AND c.workspaceId = "{workspace_id}"' + query = 'SELECT * FROM c WHERE c.deploymentStatus != @deletedStatus AND c.resourceType = @resourceType AND c.workspaceId = @workspaceId' + parameters = [ + {'name': '@deletedStatus', 'value': Status.Deleted}, + {'name': '@resourceType', 'value': ResourceType.WorkspaceService}, + {'name': '@workspaceId', 'value': workspace_id} + ] + return query, parameters async def get_active_workspace_services_for_workspace(self, workspace_id: str) -> List[WorkspaceService]: """ returns list of "non-deleted" workspace services linked to this workspace """ - query = WorkspaceServiceRepository.active_workspace_services_query(workspace_id) - workspace_services = await self.query(query=query) + query, parameters = WorkspaceServiceRepository.active_workspace_services_query(str(workspace_id)) + workspace_services = await self.query(query=query, parameters=parameters) return parse_obj_as(List[WorkspaceService], workspace_services) async def get_deployed_workspace_service_by_id(self, workspace_id: str, service_id: str, operations_repo: OperationRepository) -> WorkspaceService: @@ -49,8 +61,10 @@ async def get_deployed_workspace_service_by_id(self, workspace_id: str, service_ return workspace_service async def get_workspace_service_by_id(self, workspace_id: str, service_id: str) -> WorkspaceService: - query = self.workspace_services_query(workspace_id) + f' AND c.id = "{service_id}"' - workspace_services = await self.query(query=query) + query, parameters = self.workspace_services_query(str(workspace_id)) + query += ' AND c.id = @serviceId' + parameters.append({'name': '@serviceId', 'value': str(service_id)}) + workspace_services = await self.query(query=query, parameters=parameters) if not workspace_services: raise EntityDoesNotExist return parse_obj_as(WorkspaceService, workspace_services[0]) diff --git a/api_app/db/repositories/workspaces.py b/api_app/db/repositories/workspaces.py index 7731710982..5be9cab979 100644 --- a/api_app/db/repositories/workspaces.py +++ b/api_app/db/repositories/workspaces.py @@ -10,7 +10,8 @@ from core import config, credentials from db.errors import EntityDoesNotExist, InvalidInput, ResourceIsNotDeployed from db.repositories.resource_templates import ResourceTemplateRepository -from db.repositories.resources import ResourceRepository, IS_NOT_DELETED_CLAUSE +from db.repositories.resources import ResourceRepository +from models.domain.operation import Status from db.repositories.operations import OperationRepository from models.domain.resource import ResourceType from models.domain.workspace import Workspace @@ -35,20 +36,29 @@ async def create(cls): @staticmethod def workspaces_query_string(): - return f'SELECT * FROM c WHERE c.resourceType = "{ResourceType.Workspace}"' + query = 'SELECT * FROM c WHERE c.resourceType = @resourceType' + parameters = [ + {'name': '@resourceType', 'value': ResourceType.Workspace} + ] + return query, parameters @staticmethod def active_workspaces_query_string(): - return f'SELECT * FROM c WHERE c.resourceType = "{ResourceType.Workspace}" AND {IS_NOT_DELETED_CLAUSE}' + query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.deploymentStatus != @deletedStatus' + parameters = [ + {'name': '@resourceType', 'value': ResourceType.Workspace}, + {'name': '@deletedStatus', 'value': Status.Deleted} + ] + return query, parameters async def get_workspaces(self) -> List[Workspace]: - query = WorkspaceRepository.workspaces_query_string() - workspaces = await self.query(query=query) + query, parameters = WorkspaceRepository.workspaces_query_string() + workspaces = await self.query(query=query, parameters=parameters) return parse_obj_as(List[Workspace], workspaces) async def get_active_workspaces(self) -> List[Workspace]: - query = WorkspaceRepository.active_workspaces_query_string() - workspaces = await self.query(query=query) + query, parameters = WorkspaceRepository.active_workspaces_query_string() + workspaces = await self.query(query=query, parameters=parameters) return parse_obj_as(List[Workspace], workspaces) async def get_deployed_workspace_by_id(self, workspace_id: str, operations_repo: OperationRepository) -> Workspace: @@ -60,8 +70,10 @@ async def get_deployed_workspace_by_id(self, workspace_id: str, operations_repo: return workspace async def get_workspace_by_id(self, workspace_id: str) -> Workspace: - query = self.workspaces_query_string() + f' AND c.id = "{workspace_id}"' - workspaces = await self.query(query=query) + query, parameters = self.workspaces_query_string() + query += ' AND c.id = @workspaceId' + parameters.append({'name': '@workspaceId', 'value': str(workspace_id)}) + workspaces = await self.query(query=query, parameters=parameters) if not workspaces: raise EntityDoesNotExist return parse_obj_as(Workspace, workspaces[0]) diff --git a/api_app/tests_ma/test_db/test_repositories/test_resource_templates_repository.py b/api_app/tests_ma/test_db/test_repositories/test_resource_templates_repository.py index 813c1b7471..4109b7cec9 100644 --- a/api_app/tests_ma/test_db/test_repositories/test_resource_templates_repository.py +++ b/api_app/tests_ma/test_db/test_repositories/test_resource_templates_repository.py @@ -76,12 +76,17 @@ async def test_create_workspace_template_succeeds_without_required(uuid_mock, sa @patch('db.repositories.resource_templates.ResourceTemplateRepository.query') async def test_get_by_name_and_version_queries_db(query_mock, resource_template_repo): - expected_query = 'SELECT * FROM c WHERE c.resourceType = "workspace" AND c.name = "test" AND c.version = "1.0"' + expected_query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.name = @name AND c.version = @version' + expected_parameters = [ + {'name': '@resourceType', 'value': ResourceType.Workspace}, + {'name': '@name', 'value': 'test'}, + {'name': '@version', 'value': '1.0'} + ] query_mock.return_value = [sample_resource_template_as_dict(name="test", version="1.0")] await resource_template_repo.get_template_by_name_and_version(name="test", version="1.0", resource_type=ResourceType.Workspace) - query_mock.assert_called_once_with(query=expected_query) + query_mock.assert_called_once_with(query=expected_query, parameters=expected_parameters) @patch('db.repositories.resource_templates.ResourceTemplateRepository.query') @@ -109,12 +114,16 @@ async def test_get_by_name_and_version_raises_entity_does_not_exist_if_no_templa @patch('db.repositories.resource_templates.ResourceTemplateRepository.query') async def test_get_current_by_name_queries_db(query_mock, resource_template_repo): template_name = "template1" - expected_query = 'SELECT * FROM c WHERE c.resourceType = "workspace" AND c.name = "template1" AND c.current = true' + expected_query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.name = @name AND c.current = true' + expected_parameters = [ + {'name': '@resourceType', 'value': ResourceType.Workspace}, + {'name': '@name', 'value': 'template1'} + ] query_mock.return_value = [sample_resource_template_as_dict(name="test")] await resource_template_repo.get_current_template(template_name=template_name, resource_type=ResourceType.Workspace) - query_mock.assert_called_once_with(query=expected_query) + query_mock.assert_called_once_with(query=expected_query, parameters=expected_parameters) @patch('db.repositories.resource_templates.ResourceTemplateRepository.query') diff --git a/api_app/tests_ma/test_db/test_repositories/test_shared_service_repository.py b/api_app/tests_ma/test_db/test_repositories/test_shared_service_repository.py index 131b7c9d78..39393b6ac8 100644 --- a/api_app/tests_ma/test_db/test_repositories/test_shared_service_repository.py +++ b/api_app/tests_ma/test_db/test_repositories/test_shared_service_repository.py @@ -62,10 +62,11 @@ async def test_get_shared_service_by_id_raises_if_does_not_exist(shared_service_ async def test_get_active_shared_services_for_shared_queries_db(shared_service_repo): shared_service_repo.query = AsyncMock(return_value=[]) + query, parameters = SharedServiceRepository.active_shared_services_query() await shared_service_repo.get_active_shared_services() - shared_service_repo.query.assert_called_once_with(query=SharedServiceRepository.active_shared_services_query()) + shared_service_repo.query.assert_called_once_with(query=query, parameters=parameters) @patch('db.repositories.shared_services.SharedServiceRepository.validate_input_against_template') diff --git a/api_app/tests_ma/test_db/test_repositories/test_shared_service_templates_repository.py b/api_app/tests_ma/test_db/test_repositories/test_shared_service_templates_repository.py index 226777f6f4..72cd247a3e 100644 --- a/api_app/tests_ma/test_db/test_repositories/test_shared_service_templates_repository.py +++ b/api_app/tests_ma/test_db/test_repositories/test_shared_service_templates_repository.py @@ -42,9 +42,12 @@ async def test_create_shared_service_template_item_calls_create_item_with_the_co @patch('db.repositories.resource_templates.ResourceTemplateRepository.query') async def test_get_templates_for_shared_services_queries_db(query_mock, resource_template_repo): - expected_query = 'SELECT c.name, c.title, c.description, c.authorizedRoles FROM c WHERE c.resourceType = "shared-service" AND c.current = true' + expected_query = 'SELECT c.name, c.title, c.description, c.authorizedRoles FROM c WHERE c.resourceType = @resourceType AND c.current = true' + expected_parameters = [ + {'name': '@resourceType', 'value': ResourceType.SharedService} + ] query_mock.return_value = [sample_resource_template_as_dict(name="test", version="1.0", resource_type=ResourceType.SharedService)] await resource_template_repo.get_templates_information(ResourceType.SharedService, parent_service_name="parent_service") - query_mock.assert_called_once_with(query=expected_query) + query_mock.assert_called_once_with(query=expected_query, parameters=expected_parameters) diff --git a/api_app/tests_ma/test_db/test_repositories/test_user_resource_repository.py b/api_app/tests_ma/test_db/test_repositories/test_user_resource_repository.py index 90067c3356..e271e6dac6 100644 --- a/api_app/tests_ma/test_db/test_repositories/test_user_resource_repository.py +++ b/api_app/tests_ma/test_db/test_repositories/test_user_resource_repository.py @@ -2,8 +2,8 @@ import pytest import pytest_asyncio +from models.domain.operation import Status from db.errors import EntityDoesNotExist -from db.repositories.resources import IS_NOT_DELETED_CLAUSE from db.repositories.user_resources import UserResourceRepository from models.domain.resource import ResourceType from models.domain.user_resource import UserResource @@ -70,11 +70,17 @@ async def test_create_user_resource_item_raises_value_error_if_template_is_inval @patch('db.repositories.user_resources.UserResourceRepository.query', return_value=[]) async def test_get_user_resources_for_workspace_queries_db(query_mock, user_resource_repo): - expected_query = f'SELECT * FROM c WHERE {IS_NOT_DELETED_CLAUSE} AND c.resourceType = "user-resource" AND c.parentWorkspaceServiceId = "{SERVICE_ID}" AND c.workspaceId = "{WORKSPACE_ID}"' + expected_query = 'SELECT * FROM c WHERE c.deploymentStatus != @deletedStatus AND c.resourceType = @resourceType AND c.parentWorkspaceServiceId = @serviceId AND c.workspaceId = @workspaceId' + expected_parameters = [ + {'name': '@deletedStatus', 'value': Status.Deleted}, + {'name': '@resourceType', 'value': ResourceType.UserResource}, + {'name': '@serviceId', 'value': SERVICE_ID}, + {'name': '@workspaceId', 'value': WORKSPACE_ID} + ] await user_resource_repo.get_user_resources_for_workspace_service(WORKSPACE_ID, SERVICE_ID) - query_mock.assert_called_once_with(query=expected_query) + query_mock.assert_called_once_with(query=expected_query, parameters=expected_parameters) @patch('db.repositories.user_resources.UserResourceRepository.query') @@ -89,11 +95,17 @@ async def test_get_user_resource_returns_resource_if_found(query_mock, user_reso @patch('db.repositories.user_resources.UserResourceRepository.query') async def test_get_user_resource_by_id_queries_db(query_mock, user_resource_repo, user_resource): query_mock.return_value = [user_resource.dict()] - expected_query = f'SELECT * FROM c WHERE c.resourceType = "user-resource" AND c.parentWorkspaceServiceId = "{SERVICE_ID}" AND c.workspaceId = "{WORKSPACE_ID}" AND c.id = "{RESOURCE_ID}"' + expected_query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.parentWorkspaceServiceId = @serviceId AND c.workspaceId = @workspaceId AND c.id = @resourceId' + expected_parameters = [ + {'name': '@resourceType', 'value': ResourceType.UserResource}, + {'name': '@serviceId', 'value': SERVICE_ID}, + {'name': '@workspaceId', 'value': WORKSPACE_ID}, + {'name': '@resourceId', 'value': RESOURCE_ID} + ] await user_resource_repo.get_user_resource_by_id(WORKSPACE_ID, SERVICE_ID, RESOURCE_ID) - query_mock.assert_called_once_with(query=expected_query) + query_mock.assert_called_once_with(query=expected_query, parameters=expected_parameters) @patch('db.repositories.user_resources.UserResourceRepository.query', return_value=[]) diff --git a/api_app/tests_ma/test_db/test_repositories/test_user_resource_templates_repository.py b/api_app/tests_ma/test_db/test_repositories/test_user_resource_templates_repository.py index 727c9a2d18..fd965801b0 100644 --- a/api_app/tests_ma/test_db/test_repositories/test_user_resource_templates_repository.py +++ b/api_app/tests_ma/test_db/test_repositories/test_user_resource_templates_repository.py @@ -35,24 +35,35 @@ def sample_user_resource_template_as_dict(name: str, version: str = "1.0") -> di @patch('db.repositories.resource_templates.ResourceTemplateRepository.query') async def test_get_user_resource_template_by_name_and_version_queries_db(query_mock, resource_template_repo): - expected_query = 'SELECT * FROM c WHERE c.resourceType = "user-resource" AND c.name = "test" AND c.version = "1.0" AND c.parentWorkspaceService = "parent_service"' + expected_query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.name = @name AND c.version = @version AND c.parentWorkspaceService = @parentWorkspaceService' + expected_parameters = [ + {'name': '@resourceType', 'value': ResourceType.UserResource}, + {'name': '@name', 'value': 'test'}, + {'name': '@version', 'value': '1.0'}, + {'name': '@parentWorkspaceService', 'value': 'parent_service'} + ] query_mock.return_value = [sample_user_resource_template_as_dict(name="test", version="1.0")] await resource_template_repo.get_template_by_name_and_version(name="test", version="1.0", resource_type=ResourceType.UserResource, parent_service_name="parent_service") - query_mock.assert_called_once_with(query=expected_query) + query_mock.assert_called_once_with(query=expected_query, parameters=expected_parameters) @patch('db.repositories.resource_templates.ResourceTemplateRepository.query') async def test_get_current_user_resource_template_queries_db(query_mock, resource_template_repo): template_name = "template1" parent_template_name = "parent_template1" - expected_query = 'SELECT * FROM c WHERE c.resourceType = "user-resource" AND c.name = "template1" AND c.current = true AND c.parentWorkspaceService = "parent_template1"' + expected_query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.name = @name AND c.current = true AND c.parentWorkspaceService = @parentWorkspaceService' + expected_parameters = [ + {'name': '@resourceType', 'value': ResourceType.UserResource}, + {'name': '@name', 'value': 'template1'}, + {'name': '@parentWorkspaceService', 'value': 'parent_template1'} + ] query_mock.return_value = [sample_user_resource_template_as_dict(name=template_name)] await resource_template_repo.get_current_template(template_name, ResourceType.UserResource, parent_template_name) - query_mock.assert_called_once_with(query=expected_query) + query_mock.assert_called_once_with(query=expected_query, parameters=expected_parameters) @patch('db.repositories.resource_templates.ResourceTemplateRepository.query') @@ -110,9 +121,13 @@ async def test_create_user_resource_template_item_calls_create_item_with_the_cor @patch('db.repositories.resource_templates.ResourceTemplateRepository.query') async def test_get_template_infos_for_user_resources_queries_db(query_mock, resource_template_repo): - expected_query = 'SELECT c.name, c.title, c.description, c.authorizedRoles FROM c WHERE c.resourceType = "user-resource" AND c.current = true AND c.parentWorkspaceService = "parent_service"' + expected_query = 'SELECT c.name, c.title, c.description, c.authorizedRoles FROM c WHERE c.resourceType = @resourceType AND c.current = true AND c.parentWorkspaceService = @parentWorkspaceService' + expected_parameters = [ + {'name': '@resourceType', 'value': ResourceType.UserResource}, + {'name': '@parentWorkspaceService', 'value': 'parent_service'} + ] query_mock.return_value = [sample_user_resource_template_as_dict(name="test", version="1.0")] await resource_template_repo.get_templates_information(ResourceType.UserResource, parent_service_name="parent_service") - query_mock.assert_called_once_with(query=expected_query) + query_mock.assert_called_once_with(query=expected_query, parameters=expected_parameters) diff --git a/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py b/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py index f20f1243b6..a72b2d85e1 100644 --- a/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py +++ b/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py @@ -48,19 +48,19 @@ def workspace(): @pytest.mark.asyncio async def test_get_workspaces_queries_db(workspace_repo): workspace_repo.container.query_items = MagicMock() - expected_query = workspace_repo.workspaces_query_string() + expected_query, expected_parameters = workspace_repo.workspaces_query_string() await workspace_repo.get_workspaces() - workspace_repo.container.query_items.assert_called_once_with(query=expected_query, parameters=None) + workspace_repo.container.query_items.assert_called_once_with(query=expected_query, parameters=expected_parameters) @pytest.mark.asyncio async def test_get_active_workspaces_queries_db(workspace_repo): workspace_repo.container.query_items = MagicMock() - expected_query = workspace_repo.active_workspaces_query_string() + expected_query, expected_parameters = workspace_repo.active_workspaces_query_string() await workspace_repo.get_active_workspaces() - workspace_repo.container.query_items.assert_called_once_with(query=expected_query, parameters=None) + workspace_repo.container.query_items.assert_called_once_with(query=expected_query, parameters=expected_parameters) @pytest.mark.asyncio @@ -89,10 +89,14 @@ async def test_get_workspace_by_id_queries_db(workspace_repo, workspace): workspace_query_item_result = AsyncMock() workspace_query_item_result.__aiter__.return_value = [workspace.dict()] workspace_repo.container.query_items = MagicMock(return_value=workspace_query_item_result) - expected_query = f'SELECT * FROM c WHERE c.resourceType = "workspace" AND c.id = "{workspace.id}"' + expected_query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.id = @workspaceId' + expected_parameters = [ + {'name': '@resourceType', 'value': ResourceType.Workspace}, + {'name': '@workspaceId', 'value': workspace.id} + ] await workspace_repo.get_workspace_by_id(workspace.id) - workspace_repo.container.query_items.assert_called_once_with(query=expected_query, parameters=None) + workspace_repo.container.query_items.assert_called_once_with(query=expected_query, parameters=expected_parameters) @pytest.mark.asyncio diff --git a/api_app/tests_ma/test_db/test_repositories/test_workpaces_service_repository.py b/api_app/tests_ma/test_db/test_repositories/test_workpaces_service_repository.py index 390c0e18d2..b3f2c808fb 100644 --- a/api_app/tests_ma/test_db/test_repositories/test_workpaces_service_repository.py +++ b/api_app/tests_ma/test_db/test_repositories/test_workpaces_service_repository.py @@ -50,10 +50,11 @@ def workspace_service(): async def test_get_active_workspace_services_for_workspace_queries_db(workspace_service_repo): workspace_service_repo.query = AsyncMock(return_value=[]) + query, parameters = WorkspaceServiceRepository.active_workspace_services_query(WORKSPACE_ID) await workspace_service_repo.get_active_workspace_services_for_workspace(WORKSPACE_ID) - workspace_service_repo.query.assert_called_once_with(query=WorkspaceServiceRepository.active_workspace_services_query(WORKSPACE_ID)) + workspace_service_repo.query.assert_called_once_with(query=query, parameters=parameters) async def test_get_deployed_workspace_service_by_id_raises_resource_is_not_deployed_if_not_deployed(workspace_service_repo, workspace_service, operations_repo): @@ -86,11 +87,16 @@ async def test_get_workspace_service_by_id_raises_entity_does_not_exist_if_no_av async def test_get_workspace_service_by_id_queries_db(workspace_service_repo, workspace_service): workspace_service_repo.query = AsyncMock(return_value=[workspace_service]) - expected_query = f'SELECT * FROM c WHERE c.resourceType = "workspace-service" AND c.workspaceId = "{WORKSPACE_ID}" AND c.id = "{SERVICE_ID}"' + expected_query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.workspaceId = @workspaceId AND c.id = @serviceId' + expected_parameters = [ + {'name': '@resourceType', 'value': ResourceType.WorkspaceService}, + {'name': '@workspaceId', 'value': WORKSPACE_ID}, + {'name': '@serviceId', 'value': SERVICE_ID} + ] await workspace_service_repo.get_workspace_service_by_id(WORKSPACE_ID, SERVICE_ID) - workspace_service_repo.query.assert_called_once_with(query=expected_query) + workspace_service_repo.query.assert_called_once_with(query=expected_query, parameters=expected_parameters) @patch('db.repositories.workspace_services.WorkspaceServiceRepository.validate_input_against_template')