diff --git a/tests/conftest.py b/tests/conftest.py index e03221b9..01d87eb0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,9 @@ from time_tracker_api.time_entries.time_entries_repository import ( TimeEntryCosmosDBRepository, ) +from time_tracker_api.activities.activities_model import ( + ActivityCosmosDBRepository, +) fake = Faker() Faker.seed() @@ -222,6 +225,17 @@ def running_time_entry( ) +@pytest.fixture(scope="module") +def activity_repository(app: Flask) -> ActivityCosmosDBRepository: + with app.app_context(): + from commons.data_access_layer.cosmos_db import init_app, cosmos_helper + + if cosmos_helper is None: + init_app(app) + + return ActivityCosmosDBRepository() + + @pytest.fixture(scope="session") def valid_jwt(app: Flask, tenant_id: str, owner_id: str) -> str: with app.app_context(): diff --git a/tests/time_tracker_api/activities/activities_model_test.py b/tests/time_tracker_api/activities/activities_model_test.py new file mode 100644 index 00000000..97080281 --- /dev/null +++ b/tests/time_tracker_api/activities/activities_model_test.py @@ -0,0 +1,65 @@ +from unittest.mock import Mock, patch +import pytest + +from commons.data_access_layer.database import EventContext +from time_tracker_api.activities.activities_model import ( + ActivityCosmosDBRepository, + ActivityCosmosDBModel, +) + + +@pytest.mark.parametrize( + "activities_ids_list,expected_result", + [ + (["id1"], "c.id IN ('id1')"), + (["id1", "id2"], "c.id IN ('id1', 'id2')"), + (["id1", "id2", "id3", "id4"], "c.id IN ('id1', 'id2', 'id3', 'id4')"), + ], +) +def test_create_sql_in_condition( + activity_repository: ActivityCosmosDBRepository, + activities_ids_list, + expected_result, +): + result = activity_repository.create_sql_in_condition(activities_ids_list) + assert expected_result == result + + +@patch( + 'time_tracker_api.activities.activities_model.ActivityCosmosDBRepository.create_sql_condition_for_visibility' +) +@patch( + 'time_tracker_api.activities.activities_model.ActivityCosmosDBRepository.create_sql_in_condition' +) +@patch( + 'time_tracker_api.activities.activities_model.ActivityCosmosDBRepository.find_partition_key_value' +) +def test_find_all_with_id_in_list( + find_partition_key_value_mock, + create_sql_in_condition_mock, + create_sql_condition_for_visibility_mock, + event_context: EventContext, + activity_repository: ActivityCosmosDBRepository, +): + expected_item = { + 'id': 'id1', + 'name': 'testing', + 'description': 'do some testing', + 'tenant_id': 'tenantid1', + } + + query_items_mock = Mock(return_value=[expected_item]) + activity_repository.container = Mock() + activity_repository.container.query_items = query_items_mock + + result = activity_repository.find_all_with_id_in_list(event_context, []) + + create_sql_condition_for_visibility_mock.assert_called_once() + create_sql_in_condition_mock.assert_called_once() + find_partition_key_value_mock.assert_called_once() + query_items_mock.assert_called_once() + + assert len(result) == 1 + activity = result[0] + assert isinstance(activity, ActivityCosmosDBModel) + assert activity.__dict__ == expected_item diff --git a/tests/utils/repository_test.py b/tests/utils/repository_test.py new file mode 100644 index 00000000..16b0b440 --- /dev/null +++ b/tests/utils/repository_test.py @@ -0,0 +1,36 @@ +from unittest.mock import patch +from utils.repository import convert_list_to_tuple_string +import pytest + + +@pytest.mark.parametrize( + "ids_list", + [ + 123, + (1, 2), + "ids_list", + {"ids_list": []}, + ], +) +def test_convert_list_to_tuple_string_should_fail(ids_list): + try: + convert_list_to_tuple_string(ids_list) + except Exception as e: + assert type(e) is AssertionError + + +@pytest.mark.parametrize( + "ids_list,expected_result", + [ + (["id1"], "('id1')"), + (["id1", "id2"], "('id1', 'id2')"), + (["id1", "id2", "id3", "id4"], "('id1', 'id2', 'id3', 'id4')"), + ], +) +def test_convert_list_to_tuple_string_should_success( + ids_list, + expected_result, +): + result = convert_list_to_tuple_string(ids_list) + + assert expected_result == result diff --git a/time_tracker_api/activities/activities_model.py b/time_tracker_api/activities/activities_model.py index 23e173e8..dfb834e8 100644 --- a/time_tracker_api/activities/activities_model.py +++ b/time_tracker_api/activities/activities_model.py @@ -2,8 +2,15 @@ from azure.cosmos import PartitionKey -from commons.data_access_layer.cosmos_db import CosmosDBModel, CosmosDBDao, CosmosDBRepository +from commons.data_access_layer.cosmos_db import ( + CosmosDBModel, + CosmosDBDao, + CosmosDBRepository, +) from time_tracker_api.database import CRUDDao, APICosmosDBDao +from typing import List, Callable +from commons.data_access_layer.database import EventContext +from utils.repository import convert_list_to_tuple_string class ActivityDao(CRUDDao): @@ -17,7 +24,7 @@ class ActivityDao(CRUDDao): 'uniqueKeys': [ {'paths': ['/name', '/deleted']}, ] - } + }, } @@ -39,12 +46,63 @@ def __str___(self): return "the activity \"%s\"" % self.name # pragma: no cover -def create_dao() -> ActivityDao: - repository = CosmosDBRepository.from_definition(container_definition, - mapper=ActivityCosmosDBModel) +class ActivityCosmosDBRepository(CosmosDBRepository): + def __init__(self): + CosmosDBRepository.__init__( + self, + container_id=container_definition['id'], + partition_key_attribute='tenant_id', + mapper=ActivityCosmosDBModel, + ) + + def create_sql_in_condition(self, activity_ids): + id_values = convert_list_to_tuple_string(activity_ids) + + return "c.id IN {value_condition}".format(value_condition=id_values) + + def find_all_with_id_in_list( + self, + event_context: EventContext, + activity_ids: List[str], + visible_only=True, + mapper: Callable = None, + ): + visibility = self.create_sql_condition_for_visibility(visible_only) + query_str = """ + SELECT * FROM c + WHERE {condition} + {visibility_condition} + """.format( + condition=self.create_sql_in_condition(activity_ids), + visibility_condition=visibility, + ) + + tenant_id_value = self.find_partition_key_value(event_context) + result = self.container.query_items( + query=query_str, + partition_key=tenant_id_value, + ) + + function_mapper = self.get_mapper_or_dict(mapper) + return list(map(function_mapper, result)) - class ActivityCosmosDBDao(APICosmosDBDao, ActivityDao): - def __init__(self): - CosmosDBDao.__init__(self, repository) - return ActivityCosmosDBDao() +class ActivityCosmosDBDao(APICosmosDBDao, ActivityDao): + def __init__(self, repository): + CosmosDBDao.__init__(self, repository) + + def get_all_with_id_in_list( + self, + activity_ids, + ): + event_ctx = self.create_event_context("read-many") + return self.repository.find_all_with_id_in_list( + event_ctx, + activity_ids, + ) + + +def create_dao() -> ActivityDao: + repository = ActivityCosmosDBRepository() + + return ActivityCosmosDBDao(repository) diff --git a/utils/repository.py b/utils/repository.py new file mode 100644 index 00000000..4c0ed5fd --- /dev/null +++ b/utils/repository.py @@ -0,0 +1,9 @@ +def convert_list_to_tuple_string(ids_list): + assert isinstance(ids_list, list) + assert len(ids_list) > 0 + result = ( + str(tuple(ids_list)).replace(",", "") + if len(ids_list) == 1 + else str(tuple(ids_list)) + ) + return result