From 9456ab84f5f5e0b804bd0011037ee72d7da49fbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Vitters=C3=B8?= Date: Wed, 9 Nov 2022 15:52:08 +0100 Subject: [PATCH] feat: allow each user to have their own todos --- api/src/entities/TodoItem.py | 1 + api/src/features/todo/todo_feature.py | 31 +++++-- api/src/features/todo/use_cases/add_todo.py | 3 +- .../features/todo/use_cases/get_todo_all.py | 13 ++- .../features/todo/use_cases/get_todo_by_id.py | 5 +- .../features/todo/use_cases/update_todo.py | 7 +- .../repositories/test_TodoRepository.py | 88 ++++++++++--------- api/src/tests/unit/entities/test_todo_item.py | 6 +- api/src/tests/unit/features/todo/conftest.py | 6 +- .../features/todo/use_cases/test_add_todo.py | 4 +- .../todo/use_cases/test_get_todo_all.py | 2 +- .../todo/use_cases/test_get_todo_by_id.py | 4 +- .../todo/use_cases/test_update_todo.py | 2 +- 13 files changed, 101 insertions(+), 71 deletions(-) diff --git a/api/src/entities/TodoItem.py b/api/src/entities/TodoItem.py index a107b9ee..8bde68ff 100644 --- a/api/src/entities/TodoItem.py +++ b/api/src/entities/TodoItem.py @@ -4,6 +4,7 @@ @dataclass(frozen=True) class TodoItem: id: str + user_id: str title: str is_completed: bool = False diff --git a/api/src/features/todo/todo_feature.py b/api/src/features/todo/todo_feature.py index fad981ff..b86ab2ae 100644 --- a/api/src/features/todo/todo_feature.py +++ b/api/src/features/todo/todo_feature.py @@ -3,6 +3,8 @@ from fastapi import APIRouter, Depends from starlette.responses import JSONResponse +from authentication.authentication import auth_with_jwt +from authentication.models import User from common.responses import create_response from data_providers.get_repository import get_todo_repository from data_providers.repository_interfaces.TodoRepositoryInterface import ( @@ -24,14 +26,22 @@ @router.post("", operation_id="create", response_model=AddTodoResponse) @create_response(JSONResponse) -def add_todo(data: AddTodoRequest, todo_repository: TodoRepositoryInterface = Depends(get_todo_repository)): - return add_todo_use_case(data=data, todo_repository=todo_repository).dict() +def add_todo( + data: AddTodoRequest, + user: User = Depends(auth_with_jwt), + todo_repository: TodoRepositoryInterface = Depends(get_todo_repository), +): + return add_todo_use_case(data=data, user_id=user.user_id, todo_repository=todo_repository).dict() @router.get("/{id}", operation_id="get_by_id", response_model=GetTodoByIdResponse) @create_response(JSONResponse) -def get_todo_by_id(id: str, todo_repository: TodoRepositoryInterface = Depends(get_todo_repository)): - return get_todo_by_id_use_case(id=id, todo_repository=todo_repository).dict() +def get_todo_by_id( + id: str, + user: User = Depends(auth_with_jwt), + todo_repository: TodoRepositoryInterface = Depends(get_todo_repository), +): + return get_todo_by_id_use_case(id=id, user_id=user.user_id, todo_repository=todo_repository).dict() @router.delete("/{id}", operation_id="delete_by_id", response_model=DeleteTodoByIdResponse) @@ -42,13 +52,18 @@ def delete_todo_by_id(id: str, todo_repository: TodoRepositoryInterface = Depend @router.get("", operation_id="get_all", response_model=List[GetTodoAllResponse]) @create_response(JSONResponse) -def get_todo_all(todo_repository: TodoRepositoryInterface = Depends(get_todo_repository)): - return [todo.dict() for todo in get_todo_all_use_case(todo_repository=todo_repository)] +def get_todo_all( + user: User = Depends(auth_with_jwt), todo_repository: TodoRepositoryInterface = Depends(get_todo_repository) +): + return [todo.dict() for todo in get_todo_all_use_case(user_id=user.user_id, todo_repository=todo_repository)] @router.put("/{id}", operation_id="update_by_id", response_model=UpdateTodoResponse) @create_response(JSONResponse) def update_todo( - id: str, data: UpdateTodoRequest, todo_repository: TodoRepositoryInterface = Depends(get_todo_repository) + id: str, + data: UpdateTodoRequest, + user: User = Depends(auth_with_jwt), + todo_repository: TodoRepositoryInterface = Depends(get_todo_repository), ): - return update_todo_use_case(id=id, data=data, todo_repository=todo_repository).dict() + return update_todo_use_case(id=id, data=data, user_id=user.user_id, todo_repository=todo_repository).dict() diff --git a/api/src/features/todo/use_cases/add_todo.py b/api/src/features/todo/use_cases/add_todo.py index bb1bbad3..fdd30f17 100644 --- a/api/src/features/todo/use_cases/add_todo.py +++ b/api/src/features/todo/use_cases/add_todo.py @@ -30,8 +30,9 @@ def from_entity(todo_item: TodoItem) -> "AddTodoResponse": def add_todo_use_case( data: AddTodoRequest, + user_id: str, todo_repository: TodoRepositoryInterface, ) -> AddTodoResponse: - todo_item = TodoItem(id=str(uuid.uuid4()), title=data.title) + todo_item = TodoItem(id=str(uuid.uuid4()), title=data.title, user_id=user_id) todo_repository.create(todo_item) return AddTodoResponse.from_entity(todo_item) diff --git a/api/src/features/todo/use_cases/get_todo_all.py b/api/src/features/todo/use_cases/get_todo_all.py index 0ba13c19..8806acf3 100644 --- a/api/src/features/todo/use_cases/get_todo_all.py +++ b/api/src/features/todo/use_cases/get_todo_all.py @@ -19,12 +19,11 @@ def from_entity(todo_item: TodoItem): def get_todo_all_use_case( + user_id: str, todo_repository: TodoRepositoryInterface, ) -> List[GetTodoAllResponse]: - response: List[GetTodoAllResponse] = [] - todo_items: List[TodoItem] = todo_repository.get_all() - - for todo_item in todo_items: - response.append(GetTodoAllResponse.from_entity(todo_item)) - - return response + return [ + GetTodoAllResponse.from_entity(todo_item) + for todo_item in todo_repository.get_all() + if todo_item.user_id == user_id + ] diff --git a/api/src/features/todo/use_cases/get_todo_by_id.py b/api/src/features/todo/use_cases/get_todo_by_id.py index 52044172..fddc16c7 100644 --- a/api/src/features/todo/use_cases/get_todo_by_id.py +++ b/api/src/features/todo/use_cases/get_todo_by_id.py @@ -2,6 +2,7 @@ from pydantic import BaseModel, Field +from common.exceptions import NotFoundException from data_providers.repository_interfaces.TodoRepositoryInterface import ( TodoRepositoryInterface, ) @@ -18,6 +19,8 @@ def from_entity(todo_item: TodoItem) -> "GetTodoByIdResponse": return GetTodoByIdResponse(id=todo_item.id, title=todo_item.title, is_completed=todo_item.is_completed) -def get_todo_by_id_use_case(id: str, todo_repository: TodoRepositoryInterface) -> GetTodoByIdResponse: +def get_todo_by_id_use_case(id: str, user_id: str, todo_repository: TodoRepositoryInterface) -> GetTodoByIdResponse: todo_item = todo_repository.get(id) + if todo_item.user_id != user_id: + raise NotFoundException return GetTodoByIdResponse.from_entity(cast(TodoItem, todo_item)) diff --git a/api/src/features/todo/use_cases/update_todo.py b/api/src/features/todo/use_cases/update_todo.py index 766c89e6..b4b65e2f 100644 --- a/api/src/features/todo/use_cases/update_todo.py +++ b/api/src/features/todo/use_cases/update_todo.py @@ -1,5 +1,6 @@ from pydantic import BaseModel, Field +from common.exceptions import NotFoundException from data_providers.repository_interfaces.TodoRepositoryInterface import ( TodoRepositoryInterface, ) @@ -23,10 +24,14 @@ class UpdateTodoResponse(BaseModel): def update_todo_use_case( id: str, data: UpdateTodoRequest, + user_id: str, todo_repository: TodoRepositoryInterface, ) -> UpdateTodoResponse: todo_item = todo_repository.get(id) - updated_todo_item = TodoItem(id=todo_item.id, title=data.title, is_completed=data.is_completed) + if todo_item.user_id != user_id: + raise NotFoundException + + updated_todo_item = TodoItem(id=todo_item.id, title=data.title, is_completed=data.is_completed, user_id=user_id) if todo_repository.update(updated_todo_item): return UpdateTodoResponse(success=True) return UpdateTodoResponse(success=False) diff --git a/api/src/tests/unit/data_providers/repositories/test_TodoRepository.py b/api/src/tests/unit/data_providers/repositories/test_TodoRepository.py index e72458e4..00ef88cc 100644 --- a/api/src/tests/unit/data_providers/repositories/test_TodoRepository.py +++ b/api/src/tests/unit/data_providers/repositories/test_TodoRepository.py @@ -3,95 +3,97 @@ from common.exceptions import NotFoundException, ValidationException from data_providers.clients.mongodb.MongoDatabaseClient import MongoDatabaseClient from data_providers.repositories.TodoRepository import TodoRepository +from data_providers.repository_interfaces.TodoRepositoryInterface import ( + TodoRepositoryInterface, +) from entities.TodoItem import TodoItem class TestTodoRepository: @pytest.fixture(autouse=True) def _setup_repository(self, test_client: MongoDatabaseClient): - self.repository = TodoRepository(client=test_client) + self.repository: TodoRepositoryInterface = TodoRepository(client=test_client) def test_create(self): - todo_item = TodoItem(id="1234", title="todo 1") + todo_item = TodoItem(id="1234", title="todo 1", user_id="xyz") self.repository.create(todo_item) assert len(self.repository.get_all()) == 1 def test_create_already_exists(self): - todo_item_1 = TodoItem(id="1234", title="todo 1") + todo_item_1 = TodoItem(id="1234", title="todo 1", user_id="xyz") self.repository.create(todo_item_1) with pytest.raises(ValidationException): - todo_item_2 = TodoItem(id="1234", title="todo 1") + todo_item_2 = TodoItem(id="1234", title="todo 1", user_id="xyz") self.repository.create(todo_item_2) def test_find_item_that_exist(self): documents = [ - {"_id": "81549300", "title": "todo 1"}, - {"_id": "1a2b", "title": "todo 2"}, - {"_id": "987321", "title": "todo 3"}, - { - "_id": "987456", - "title": "todo 4", - }, + {"_id": "81549300", "title": "todo 1", "user_id": "xyz"}, + {"_id": "1a2b", "title": "todo 2", "user_id": "xyz"}, + {"_id": "987321", "title": "todo 3", "user_id": "abc"}, + {"_id": "987456", "title": "todo 4", "user_id": "abc"}, ] self.repository.client.insert_many(documents) - todo_item = self.repository.find_one({"title": "todo 2"}) - assert todo_item.id == "1a2b" + assert self.repository.find_one({"title": "todo 2", "user_id": "xyz"}).id == "1a2b" def test_find_item_that_does_not_exist(self): documents = [ - {"_id": "81549300", "title": "todo 1"}, - {"_id": "1a2b", "title": "todo 2"}, - {"_id": "987321", "title": "todo 3"}, - { - "_id": "987456", - "title": "todo 4", - }, + {"_id": "81549300", "title": "todo 1", "user_id": "xyz"}, + {"_id": "1a2b", "title": "todo 2", "user_id": "xyz"}, + {"_id": "987321", "title": "todo 3", "user_id": "abc"}, + {"_id": "987456", "title": "todo 4", "user_id": "abc"}, ] self.repository.client.insert_many(documents) assert self.repository.find_one({"_id": "invalid"}) is None + def test_find_item_of_other_user(self): + documents = [ + {"_id": "81549300", "title": "todo 1", "user_id": "xyz"}, + {"_id": "1a2b", "title": "todo 2", "user_id": "xyz"}, + {"_id": "987321", "title": "todo 3", "user_id": "abc"}, + {"_id": "987456", "title": "todo 4", "user_id": "abc"}, + ] + self.repository.client.insert_many(documents) + assert self.repository.find_one({"_id": "1a2b", "user_id": "abc"}) is None + def test_get_item_that_does_exist(self): documents = [ - {"_id": "81549300", "title": "todo 1"}, - {"_id": "1a2b", "title": "todo 2"}, - {"_id": "987321", "title": "todo 3"}, - { - "_id": "987456", - "title": "todo 4", - }, + {"_id": "81549300", "title": "todo 1", "user_id": "xyz"}, + {"_id": "1a2b", "title": "todo 2", "user_id": "xyz"}, + {"_id": "987321", "title": "todo 3", "user_id": "abc"}, + {"_id": "987456", "title": "todo 4", "user_id": "abc"}, ] self.repository.client.insert_many(documents) - todo_item = self.repository.get("987321") - assert todo_item.id == "987321" + assert self.repository.get("987321").id == "987321" def test_get_item_that_does_not_exist(self): documents = [ - {"_id": "81549300", "title": "todo 1"}, - {"_id": "1a2b", "title": "todo 2"}, - {"_id": "987321", "title": "todo 3"}, - { - "_id": "987456", - "title": "todo 4", - }, + {"_id": "81549300", "title": "todo 1", "user_id": "xyz"}, + {"_id": "1a2b", "title": "todo 2", "user_id": "xyz"}, + {"_id": "987321", "title": "todo 3", "user_id": "abc"}, + {"_id": "987456", "title": "todo 4", "user_id": "abc"}, ] self.repository.client.insert_many(documents) with pytest.raises(NotFoundException): self.repository.get("invalid") def test_update_item(self): - todo_item = TodoItem(id="81549300", title="todo 1") + todo_item = TodoItem(id="81549300", title="todo 1", user_id="xyz") self.repository.create(todo_item) - todo_item_to_update = TodoItem(id="81549300", title="Updated title") + todo_item_to_update = TodoItem(id="81549300", title="Updated title", user_id="xyz") self.repository.update(todo_item=todo_item_to_update) assert self.repository.get("81549300").title == "Updated title" def test_update_item_that_does_not_exist(self): - todo_item_to_update = TodoItem(id="unknown", title="Updated title") + todo_item_to_update = TodoItem(id="unknown", title="Updated title", user_id="xyz") with pytest.raises(NotFoundException): self.repository.update(todo_item_to_update) def test_delete(self): - documents = [{"_id": "81549300", "title": "todo 1"}, {"_id": "1a2b", "title": "todo 2"}] + documents = [ + {"_id": "81549300", "title": "todo 1", "user_id": "xyz"}, + {"_id": "1a2b", "title": "todo 2", "user_id": "xyz"}, + ] self.repository.client.insert_many(documents) assert len(self.repository.get_all()) == 2 self.repository.delete("81549300") @@ -99,7 +101,11 @@ def test_delete(self): assert self.repository.get_all() == [self.repository.get("1a2b")] def test_delete_all(self): - documents = [{"_id": "81549300", "title": "todo 1"}, {"_id": "1a2b", "title": "todo 2"}] + documents = [ + {"_id": "81549300", "title": "todo 1", "user_id": "xyz"}, + {"_id": "1a2b", "title": "todo 2", "user_id": "xyz"}, + ] self.repository.client.insert_many(documents) assert len(self.repository.get_all()) == 2 self.repository.delete_all() + assert len(self.repository.get_all()) == 0 diff --git a/api/src/tests/unit/entities/test_todo_item.py b/api/src/tests/unit/entities/test_todo_item.py index 068bf448..2e7b9dce 100644 --- a/api/src/tests/unit/entities/test_todo_item.py +++ b/api/src/tests/unit/entities/test_todo_item.py @@ -5,7 +5,7 @@ def test_todo_item_init(): id = str(uuid.uuid4()) - todo = TodoItem(id=id, title="title 1", is_completed=False) + todo = TodoItem(id=id, title="title 1", is_completed=False, user_id="xyz") assert todo.id == id assert todo.title == "title 1" assert not todo.is_completed @@ -13,7 +13,7 @@ def test_todo_item_init(): def test_todo_item_from_dict(): id = str(uuid.uuid4()) - init_dict = {"id": id, "title": "title 1", "is_completed": False} + init_dict = {"id": id, "title": "title 1", "is_completed": False, "user_id": "xyz"} todo = TodoItem.from_dict(init_dict) assert todo.id == id @@ -23,7 +23,7 @@ def test_todo_item_from_dict(): def test_todo_item_comparison(): id = str(uuid.uuid4()) - init_dict = {"id": id, "title": "title 1", "is_completed": False} + init_dict = {"id": id, "title": "title 1", "is_completed": False, "user_id": "xyz"} todo1 = TodoItem.from_dict(init_dict) todo2 = TodoItem.from_dict(init_dict) diff --git a/api/src/tests/unit/features/todo/conftest.py b/api/src/tests/unit/features/todo/conftest.py index 8b3dd4b9..0b275818 100644 --- a/api/src/tests/unit/features/todo/conftest.py +++ b/api/src/tests/unit/features/todo/conftest.py @@ -8,9 +8,9 @@ @pytest.fixture(scope="function") def todo_test_data() -> Dict[str, dict]: return { - "dh2109": {"_id": "dh2109", "title": "item 1", "is_completed": False}, - "1417b8": {"_id": "1417b8", "title": "item 2", "is_completed": True}, - "abcdefg": {"_id": "abcdefg", "title": "item 3", "is_completed": False}, + "dh2109": {"_id": "dh2109", "title": "item 1", "is_completed": False, "user_id": "xyz"}, + "1417b8": {"_id": "1417b8", "title": "item 2", "is_completed": True, "user_id": "xyz"}, + "abcdefg": {"_id": "abcdefg", "title": "item 3", "is_completed": False, "user_id": "xyz"}, } diff --git a/api/src/tests/unit/features/todo/use_cases/test_add_todo.py b/api/src/tests/unit/features/todo/use_cases/test_add_todo.py index 8d12a946..e2d0b7e2 100644 --- a/api/src/tests/unit/features/todo/use_cases/test_add_todo.py +++ b/api/src/tests/unit/features/todo/use_cases/test_add_todo.py @@ -9,11 +9,11 @@ def test_add_with_valid_title_should_return_todo(todo_repository: TodoRepositoryInterface): data = AddTodoRequest(title="new todo") - result = add_todo_use_case(data, todo_repository=todo_repository) + result = add_todo_use_case(data, user_id="xyz", todo_repository=todo_repository) assert result.title == data.title def test_add_with_empty_title_should_throw_validation_error(todo_repository: TodoRepositoryInterface): with pytest.raises(ValidationError): data = AddTodoRequest(title="") - add_todo_use_case(data, todo_repository=todo_repository) + add_todo_use_case(data, user_id="xyz", todo_repository=todo_repository) diff --git a/api/src/tests/unit/features/todo/use_cases/test_get_todo_all.py b/api/src/tests/unit/features/todo/use_cases/test_get_todo_all.py index e90b7769..11d25a9b 100644 --- a/api/src/tests/unit/features/todo/use_cases/test_get_todo_all.py +++ b/api/src/tests/unit/features/todo/use_cases/test_get_todo_all.py @@ -7,5 +7,5 @@ def test_get_todos_should_return_todos(todo_repository: TodoRepositoryInterface, todo_test_data: Dict[str, dict]): - todos = get_todo_all_use_case(todo_repository=todo_repository) + todos = get_todo_all_use_case(user_id="xyz", todo_repository=todo_repository) assert len(todos) == len(todo_test_data.keys()) diff --git a/api/src/tests/unit/features/todo/use_cases/test_get_todo_by_id.py b/api/src/tests/unit/features/todo/use_cases/test_get_todo_by_id.py index 340817e2..3eef9b66 100644 --- a/api/src/tests/unit/features/todo/use_cases/test_get_todo_by_id.py +++ b/api/src/tests/unit/features/todo/use_cases/test_get_todo_by_id.py @@ -14,11 +14,11 @@ def test_get_todo_by_id_should_return_todo(todo_repository: TodoRepositoryInterface, todo_test_data: Dict[str, dict]): id = "dh2109" - todo: GetTodoByIdResponse = get_todo_by_id_use_case(id, todo_repository=todo_repository) + todo: GetTodoByIdResponse = get_todo_by_id_use_case(id, user_id="xyz", todo_repository=todo_repository) assert todo.title == todo_test_data[id]["title"] def test_get_todo_by_id_should_throw_todo_not_found_error(todo_repository: TodoRepositoryInterface): id = "unknown" with pytest.raises(NotFoundException): - get_todo_by_id_use_case(id, todo_repository=todo_repository) + get_todo_by_id_use_case(id, user_id="xyz", todo_repository=todo_repository) diff --git a/api/src/tests/unit/features/todo/use_cases/test_update_todo.py b/api/src/tests/unit/features/todo/use_cases/test_update_todo.py index cd679989..47381b14 100644 --- a/api/src/tests/unit/features/todo/use_cases/test_update_todo.py +++ b/api/src/tests/unit/features/todo/use_cases/test_update_todo.py @@ -7,5 +7,5 @@ def test_update_todo_should_return_success(todo_repository: TodoRepositoryInterface): id = "dh2109" data = UpdateTodoRequest(title="new title", is_completed=False) - result = update_todo_use_case(id, data, todo_repository=todo_repository) + result = update_todo_use_case(id, data, user_id="xyz", todo_repository=todo_repository) assert result.success