diff --git a/tests/conftest.py b/tests/conftest.py index 155c29b6..97b80b65 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ from flask import Flask from flask.testing import FlaskClient -from commons.data_access_layer.cosmos_db import CosmosDBRepository +from commons.data_access_layer.cosmos_db import CosmosDBRepository, datetime_str, current_datetime from time_tracker_api import create_app from time_tracker_api.time_entries.time_entries_model import TimeEntryCosmosDBRepository @@ -128,6 +128,23 @@ def another_item(cosmos_db_repository: CosmosDBRepository, tenant_id: str) -> di return cosmos_db_repository.create(sample_item_data) -@pytest.yield_fixture(scope="module") +@pytest.fixture(scope="module") def time_entry_repository() -> TimeEntryCosmosDBRepository: return TimeEntryCosmosDBRepository() + + +@pytest.yield_fixture(scope="module") +def running_time_entry(time_entry_repository: TimeEntryCosmosDBRepository, + owner_id: str, + tenant_id: str): + created_time_entry = time_entry_repository.create({ + "project_id": fake.uuid4(), + "start_date": datetime_str(current_datetime()), + "owner_id": owner_id, + "tenant_id": tenant_id + }) + + yield created_time_entry + + time_entry_repository.delete(id=created_time_entry.id, + partition_key_value=tenant_id) diff --git a/tests/time_tracker_api/time_entries/time_entries_model_test.py b/tests/time_tracker_api/time_entries/time_entries_model_test.py index 4d0f42e0..2b58c036 100644 --- a/tests/time_tracker_api/time_entries/time_entries_model_test.py +++ b/tests/time_tracker_api/time_entries/time_entries_model_test.py @@ -78,3 +78,19 @@ def test_find_interception_should_ignore_id_of_existing_item(owner_id: str, assert not any([existing_item.id == item.id for item in non_colliding_result]) finally: time_entry_repository.delete_permanently(existing_item.id, partition_key_value=existing_item.tenant_id) + + +def test_find_running_should_return_running_time_entry(running_time_entry, + time_entry_repository: TimeEntryCosmosDBRepository): + found_time_entry = time_entry_repository.find_running(partition_key_value=running_time_entry.tenant_id) + + assert found_time_entry is not None + assert found_time_entry.id == running_time_entry.id + + +def test_find_running_should_not_find_any_item(tenant_id: str, + time_entry_repository: TimeEntryCosmosDBRepository): + try: + time_entry_repository.find_running(partition_key_value=tenant_id) + except Exception as e: + assert type(e) is StopIteration diff --git a/tests/time_tracker_api/time_entries/time_entries_namespace_test.py b/tests/time_tracker_api/time_entries/time_entries_namespace_test.py index 975797e5..34d34259 100644 --- a/tests/time_tracker_api/time_entries/time_entries_namespace_test.py +++ b/tests/time_tracker_api/time_entries/time_entries_namespace_test.py @@ -25,7 +25,8 @@ fake_time_entry = ({ "id": fake.random_int(1, 9999), "running": True, -}).update(valid_time_entry_input) +}) +fake_time_entry.update(valid_time_entry_input) def test_create_time_entry_with_invalid_date_range_should_raise_bad_request_error(client: FlaskClient, @@ -309,3 +310,29 @@ def test_restart_time_entry_with_id_with_invalid_format(client: FlaskClient, moc changes={"end_date": None}, partition_key_value=current_user_tenant_id(), peeker=ANY) + + +def test_get_running_should_call_find_running(client: FlaskClient, mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + repository_update_mock = mocker.patch.object(time_entries_dao.repository, + 'find_running', + return_value=fake_time_entry) + + response = client.get("/time-entries/running", follow_redirects=True) + + assert HTTPStatus.OK == response.status_code + assert json.loads(response.data) is not None + repository_update_mock.assert_called_once_with(partition_key_value=current_user_tenant_id()) + + +def test_get_running_should_return_not_found_if_find_running_throws_StopIteration(client: FlaskClient, + mocker: MockFixture): + from time_tracker_api.time_entries.time_entries_namespace import time_entries_dao + repository_update_mock = mocker.patch.object(time_entries_dao.repository, + 'find_running', + side_effect=StopIteration) + + response = client.get("/time-entries/running", follow_redirects=True) + + assert HTTPStatus.NOT_FOUND == response.status_code + repository_update_mock.assert_called_once_with(partition_key_value=current_user_tenant_id()) diff --git a/time_tracker_api/api.py b/time_tracker_api/api.py index ccf637ac..080dea08 100644 --- a/time_tracker_api/api.py +++ b/time_tracker_api/api.py @@ -77,7 +77,8 @@ def handle_cosmos_resource_exists_error(error): @api.errorhandler(CosmosResourceNotFoundError) -def handle_cosmos_resource_not_found_error(error): +@api.errorhandler(StopIteration) +def handle_not_found_errors(error): return {'message': 'It was not found'}, HTTPStatus.NOT_FOUND diff --git a/time_tracker_api/time_entries/time_entries_model.py b/time_tracker_api/time_entries/time_entries_model.py index 9145f6db..f2646edc 100644 --- a/time_tracker_api/time_entries/time_entries_model.py +++ b/time_tracker_api/time_entries/time_entries_model.py @@ -1,3 +1,4 @@ +import abc from dataclasses import dataclass, field from typing import List, Callable @@ -15,6 +16,10 @@ class TimeEntriesDao(CRUDDao): def current_user_id(): return current_user_id() + @abc.abstractmethod + def find_running(self): + pass + container_definition = { 'id': 'time_entry', @@ -102,6 +107,19 @@ def find_interception_with_date_range(self, start_date, end_date, owner_id, part function_mapper = self.get_mapper_or_dict(mapper) return list(map(function_mapper, result)) + def find_running(self, partition_key_value: str, mapper: Callable = None): + result = self.container.query_items( + query=""" + SELECT * from c + WHERE NOT IS_DEFINED(c.end_date) OR c.end_date = null + OFFSET 0 LIMIT 1 + """, + partition_key=partition_key_value, + max_item_count=1) + + function_mapper = self.get_mapper_or_dict(mapper) + return function_mapper(next(result)) + def validate_data(self, data): if data.get('end_date') is not None: if data['end_date'] <= data.get('start_date'): @@ -156,6 +174,9 @@ def delete(self, id): self.repository.delete(id, partition_key_value=self.partition_key_value, peeker=self.check_whether_current_user_owns_item) + def find_running(self): + return self.repository.find_running(partition_key_value=self.partition_key_value) + def create_dao() -> TimeEntriesDao: repository = TimeEntryCosmosDBRepository() diff --git a/time_tracker_api/time_entries/time_entries_namespace.py b/time_tracker_api/time_entries/time_entries_namespace.py index 0b65c25a..a7bc2a42 100644 --- a/time_tracker_api/time_entries/time_entries_namespace.py +++ b/time_tracker_api/time_entries/time_entries_namespace.py @@ -171,7 +171,7 @@ def post(self, id): @ns.route('//restart') @ns.response(HTTPStatus.NOT_FOUND, 'Stopped time entry not found') -@ns.response(HTTPStatus.UNPROCESSABLE_ENTITY, 'The id has an invalid format.') +@ns.response(HTTPStatus.UNPROCESSABLE_ENTITY, 'The id has an invalid format') @ns.param('id', 'The unique identifier of a stopped time entry') class RestartTimeEntry(Resource): @ns.doc('restart_time_entry') @@ -181,3 +181,14 @@ def post(self, id): return time_entries_dao.update(id, { 'end_date': None }) + + +@ns.route('/running') +@ns.response(HTTPStatus.OK, 'The time entry that is active: currently running') +@ns.response(HTTPStatus.NOT_FOUND, 'There is no time entry running right now') +class ActiveTimeEntry(Resource): + @ns.doc('running_time_entry') + @ns.marshal_with(time_entry) + def get(self): + """Find the time entry that is running""" + return time_entries_dao.find_running()