Skip to content
Merged
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
21 changes: 19 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
16 changes: 16 additions & 0 deletions tests/time_tracker_api/time_entries/time_entries_model_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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())
3 changes: 2 additions & 1 deletion time_tracker_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
21 changes: 21 additions & 0 deletions time_tracker_api/time_entries/time_entries_model.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import abc
from dataclasses import dataclass, field
from typing import List, Callable

Expand All @@ -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',
Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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()
Expand Down
13 changes: 12 additions & 1 deletion time_tracker_api/time_entries/time_entries_namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def post(self, id):

@ns.route('/<string:id>/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')
Expand All @@ -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()