From 7e9bc7bbab6b559093a07d5a1e62083fb02b5acc Mon Sep 17 00:00:00 2001 From: Achintya-Chatterjee Date: Wed, 10 Dec 2025 01:05:24 +0530 Subject: [PATCH 1/7] test(tasks): cover team + assignee filtering scenarios - add serializer, view, service, and repository specs for multi-assignee queries - exercise team-scoped assignee resolution and list/count symmetry - protect regressions where team filters should bypass user-level fallbacks --- .../unit/repositories/test_task_repository.py | 193 ++++++++++++++++++ .../serializers/test_get_tasks_serializer.py | 19 +- todo/tests/unit/services/test_task_service.py | 111 +++++++++- todo/tests/unit/views/test_task.py | 54 +++++ 4 files changed, 366 insertions(+), 11 deletions(-) diff --git a/todo/tests/unit/repositories/test_task_repository.py b/todo/tests/unit/repositories/test_task_repository.py index f24029a1..0758efa5 100644 --- a/todo/tests/unit/repositories/test_task_repository.py +++ b/todo/tests/unit/repositories/test_task_repository.py @@ -91,6 +91,117 @@ def test_list_returns_empty_list_for_no_tasks(self): self.mock_collection.find.return_value.sort.return_value.skip.assert_called_once_with(10) self.mock_collection.find.return_value.sort.return_value.skip.return_value.limit.assert_called_once_with(10) + @patch.object(TaskRepository, "_get_task_ids_for_assignees", return_value=[]) + def test_list_returns_empty_when_assignee_filter_has_no_matches(self, mock_get_task_ids): + result = TaskRepository.list( + 1, + 10, + sort_by=SORT_FIELD_CREATED_AT, + order=SORT_ORDER_DESC, + user_id=None, + assignee_ids=["user1"], + ) + + self.assertEqual(result, []) + mock_get_task_ids.assert_called_once_with(["user1"], team_id=None) + self.mock_collection.find.assert_not_called() + + @patch.object(TaskRepository, "_get_task_ids_for_assignees") + def test_list_filters_by_assignee_ids(self, mock_get_task_ids): + assignee_task_id = ObjectId() + mock_get_task_ids.return_value = [assignee_task_id] + + mock_cursor = MagicMock() + mock_cursor.__iter__ = MagicMock(return_value=iter(self.task_data)) + self.mock_collection.find.return_value.sort.return_value.skip.return_value.limit.return_value = mock_cursor + + TaskRepository.list( + 1, + 10, + sort_by=SORT_FIELD_CREATED_AT, + order=SORT_ORDER_DESC, + user_id=None, + assignee_ids=["user1"], + ) + + mock_get_task_ids.assert_called_once_with(["user1"], team_id=None) + self.mock_collection.find.assert_called_once() + query_filter = self.mock_collection.find.call_args[0][0] + self.assertIn("$and", query_filter) + self.assertTrue( + any(condition.get("_id", {}).get("$in") == [assignee_task_id] for condition in query_filter["$and"]) + ) + + @patch.object(TaskRepository, "_get_assigned_task_ids_for_user", return_value=[]) + def test_list_includes_created_tasks_when_user_has_no_assignments(self, mock_get_assigned): + mock_cursor = MagicMock() + mock_cursor.__iter__ = MagicMock(return_value=iter(self.task_data)) + self.mock_collection.find.return_value.sort.return_value.skip.return_value.limit.return_value = mock_cursor + + user_id = "user_creator" + TaskRepository.list(1, 10, sort_by="createdAt", order="desc", user_id=user_id) + + self.mock_collection.find.assert_called_once() + query_filter = self.mock_collection.find.call_args[0][0] + self.assertIn("$and", query_filter) + self.assertTrue( + any("$or" in condition and {"createdBy": user_id} in condition["$or"] for condition in query_filter["$and"]) + ) + mock_get_assigned.assert_called_once_with(user_id) + + @patch.object(TaskRepository, "_get_team_task_ids", return_value=[]) + @patch.object(TaskRepository, "_get_task_ids_for_assignees") + def test_list_with_team_and_assignee_ids_relies_on_assignee_tasks(self, mock_get_task_ids, mock_get_team_tasks): + assignee_task_id = ObjectId() + mock_get_task_ids.return_value = [assignee_task_id] + + mock_cursor = MagicMock() + mock_cursor.__iter__ = MagicMock(return_value=iter(self.task_data)) + self.mock_collection.find.return_value.sort.return_value.skip.return_value.limit.return_value = mock_cursor + + TaskRepository.list( + 1, + 10, + sort_by=SORT_FIELD_CREATED_AT, + order=SORT_ORDER_DESC, + user_id=None, + team_id="team123", + assignee_ids=["user1"], + ) + + mock_get_team_tasks.assert_not_called() # team filtering is handled by assignee helper when both provided + mock_get_task_ids.assert_called_once_with(["user1"], team_id="team123") + self.mock_collection.find.assert_called_once() + query_filter = self.mock_collection.find.call_args[0][0] + self.assertIn("$and", query_filter) + self.assertTrue( + any(condition.get("_id", {}).get("$in") == [assignee_task_id] for condition in query_filter["$and"]) + ) + + @patch.object(TaskRepository, "_get_team_task_ids", return_value=[ObjectId()]) + @patch.object(TaskRepository, "_get_assigned_task_ids_for_user") + def test_list_team_filter_skips_user_filter(self, mock_get_assigned, mock_get_team_tasks): + mock_cursor = MagicMock() + mock_cursor.__iter__ = MagicMock(return_value=iter(self.task_data)) + self.mock_collection.find.return_value.sort.return_value.skip.return_value.limit.return_value = mock_cursor + + TaskRepository.list( + 1, + 10, + sort_by=SORT_FIELD_CREATED_AT, + order=SORT_ORDER_DESC, + user_id="user123", + team_id="team123", + ) + + mock_get_team_tasks.assert_called_once() + mock_get_assigned.assert_not_called() + query_filter = self.mock_collection.find.call_args[0][0] + self.assertIn("$and", query_filter) + # Ensure no user OR clause present + for condition in query_filter["$and"]: + self.assertFalse("$or" in condition and {"createdBy": "user123"} in condition["$or"]) + def test_count_returns_total_task_count(self): self.mock_collection.count_documents.return_value = 42 @@ -122,6 +233,88 @@ def test_get_all_returns_empty_list_for_no_tasks(self): self.assertEqual(result, []) self.mock_collection.find.assert_called_once() + @patch("todo.repositories.task_repository.TaskAssignmentRepository.get_collection") + def test_get_task_ids_for_assignees_returns_object_ids(self, mock_get_collection): + mock_collection = MagicMock() + task_id_obj = ObjectId() + task_id_str = str(ObjectId()) + mock_collection.find.return_value = [{"task_id": task_id_obj}, {"task_id": task_id_str}] + mock_get_collection.return_value = mock_collection + + result = TaskRepository._get_task_ids_for_assignees([str(ObjectId()), "not-an-object-id"]) + + self.assertEqual( + set(result), + {task_id_obj, ObjectId(task_id_str)}, + ) + mock_collection.find.assert_called_once() + + @patch("todo.repositories.task_repository.TaskAssignmentRepository.get_collection") + def test_get_task_ids_for_assignees_filters_by_team(self, mock_get_collection): + mock_collection = MagicMock() + mock_collection.find.return_value = [{"task_id": ObjectId()}] + mock_get_collection.return_value = mock_collection + + team_id = str(ObjectId()) + TaskRepository._get_task_ids_for_assignees(["user1"], team_id=team_id) + + assignment_filter = mock_collection.find.call_args[0][0] + self.assertIn("team_id", assignment_filter) + values = assignment_filter["team_id"]["$in"] + self.assertIn(team_id, values) + self.assertTrue(any(ObjectId.is_valid(v) for v in values)) + + @patch.object(TaskRepository, "_get_task_ids_for_assignees", return_value=[]) + def test_count_returns_zero_when_assignee_filter_has_no_matches(self, mock_get_task_ids): + result = TaskRepository.count(assignee_ids=["user1"]) + + self.assertEqual(result, 0) + mock_get_task_ids.assert_called_once_with(["user1"], team_id=None) + self.mock_collection.count_documents.assert_not_called() + + @patch.object(TaskRepository, "_get_task_ids_for_assignees") + def test_count_filters_by_assignee_ids(self, mock_get_task_ids): + assignee_task_id = ObjectId() + mock_get_task_ids.return_value = [assignee_task_id] + self.mock_collection.count_documents.return_value = 5 + + TaskRepository.count(assignee_ids=["user1"]) + + mock_get_task_ids.assert_called_once_with(["user1"], team_id=None) + self.mock_collection.count_documents.assert_called_once() + query_filter = self.mock_collection.count_documents.call_args[0][0] + self.assertIn("$and", query_filter) + self.assertTrue( + any(condition.get("_id", {}).get("$in") == [assignee_task_id] for condition in query_filter["$and"]) + ) + + @patch.object(TaskRepository, "_get_team_task_ids", return_value=[]) + @patch.object(TaskRepository, "_get_task_ids_for_assignees") + def test_count_with_team_and_assignee_ids_relies_on_assignee_tasks(self, mock_get_task_ids, mock_get_team_tasks): + assignee_task_id = ObjectId() + mock_get_task_ids.return_value = [assignee_task_id] + self.mock_collection.count_documents.return_value = 1 + + TaskRepository.count(team_id="team123", assignee_ids=["user1"]) + + mock_get_team_tasks.assert_not_called() + mock_get_task_ids.assert_called_once_with(["user1"], team_id="team123") + self.mock_collection.count_documents.assert_called_once() + + @patch.object(TaskRepository, "_get_team_task_ids", return_value=[ObjectId()]) + @patch.object(TaskRepository, "_get_assigned_task_ids_for_user") + def test_count_team_filter_skips_user_filter(self, mock_get_assigned, mock_get_team_tasks): + self.mock_collection.count_documents.return_value = 1 + + TaskRepository.count(team_id="team123", user_id="user123") + + mock_get_team_tasks.assert_called_once() + mock_get_assigned.assert_not_called() + query_filter = self.mock_collection.count_documents.call_args[0][0] + self.assertIn("$and", query_filter) + for condition in query_filter["$and"]: + self.assertFalse("$or" in condition and {"createdBy": "user123"} in condition["$or"]) + def test_get_by_id_returns_task_model_when_found(self): task_id_str = str(self.task_db_data_fixture["_id"]) self.mock_collection.find_one.return_value = self.task_db_data_fixture diff --git a/todo/tests/unit/serializers/test_get_tasks_serializer.py b/todo/tests/unit/serializers/test_get_tasks_serializer.py index c2423dbf..9afbce3f 100644 --- a/todo/tests/unit/serializers/test_get_tasks_serializer.py +++ b/todo/tests/unit/serializers/test_get_tasks_serializer.py @@ -1,6 +1,7 @@ from unittest import TestCase -from rest_framework.exceptions import ValidationError from django.conf import settings +from django.http import QueryDict +from rest_framework.exceptions import ValidationError from todo.serializers.get_tasks_serializer import GetTaskQueryParamsSerializer from todo.constants.task import ( @@ -71,6 +72,22 @@ def test_serializer_ignores_undefined_extra_fields(self): self.assertEqual(serializer.validated_data["limit"], 5) self.assertNotIn("extra_field", serializer.validated_data) + def test_serializer_collects_assignee_ids_from_querydict(self): + query_params = QueryDict(mutable=True) + query_params.setlist("assigneeId", ["user1", "user2"]) + + serializer = GetTaskQueryParamsSerializer(data=query_params) + self.assertTrue(serializer.is_valid()) + self.assertEqual(serializer.validated_data["assignee_ids"], ["user1", "user2"]) + + def test_serializer_deduplicates_assignee_ids(self): + query_params = QueryDict(mutable=True) + query_params.setlist("assigneeId", ["user1", "user1", "user2"]) + + serializer = GetTaskQueryParamsSerializer(data=query_params) + self.assertTrue(serializer.is_valid()) + self.assertEqual(serializer.validated_data["assignee_ids"], ["user1", "user2"]) + def test_serializer_uses_django_settings_values(self): """Test that the serializer correctly uses values from Django settings""" # Instead of mocking, we'll test against the actual settings values diff --git a/todo/tests/unit/services/test_task_service.py b/todo/tests/unit/services/test_task_service.py index e15c98be..49185758 100644 --- a/todo/tests/unit/services/test_task_service.py +++ b/todo/tests/unit/services/test_task_service.py @@ -72,9 +72,16 @@ def test_get_tasks_returns_paginated_response( ) mock_list.assert_called_once_with( - 2, 1, "createdAt", "desc", str(self.user_id), team_id=None, status_filter=None + 2, + 1, + "createdAt", + "desc", + str(self.user_id), + team_id=None, + status_filter=None, + assignee_ids=None, ) - mock_count.assert_called_once() + mock_count.assert_called_once_with(str(self.user_id), team_id=None, status_filter=None, assignee_ids=None) @patch("todo.services.task_service.UserRepository.get_by_id") @patch("todo.services.task_service.TaskRepository.count") @@ -113,8 +120,50 @@ def test_get_tasks_returns_empty_response_if_no_tasks_present(self, mock_list: M self.assertEqual(len(response.tasks), 0) self.assertIsNone(response.links) - mock_list.assert_called_once_with(1, 10, "createdAt", "desc", "test_user", team_id=None, status_filter=None) - mock_count.assert_called_once() + mock_list.assert_called_once_with( + 1, + 10, + "createdAt", + "desc", + "test_user", + team_id=None, + status_filter=None, + assignee_ids=None, + ) + mock_count.assert_called_once_with("test_user", team_id=None, status_filter=None, assignee_ids=None) + + @patch("todo.services.task_service.TaskRepository.count") + @patch("todo.services.task_service.TaskRepository.list") + def test_get_tasks_passes_assignee_ids_to_repo(self, mock_list: Mock, mock_count: Mock): + mock_list.return_value = [] + mock_count.return_value = 0 + + TaskService.get_tasks( + page=1, + limit=10, + sort_by="createdAt", + order="desc", + user_id="request_user", + team_id="team123", + assignee_ids=["user1", "user2"], + ) + + mock_list.assert_called_once_with( + 1, + 10, + "createdAt", + "desc", + "request_user", + team_id="team123", + status_filter=None, + assignee_ids=["user1", "user2"], + ) + mock_count.assert_called_once_with( + "request_user", + team_id="team123", + status_filter=None, + assignee_ids=["user1", "user2"], + ) @patch("todo.services.task_service.TaskRepository.count") @patch("todo.services.task_service.TaskRepository.list") @@ -434,7 +483,14 @@ def test_get_tasks_default_sorting(self, mock_list, mock_count): TaskService.get_tasks(page=1, limit=20, sort_by="createdAt", order="desc", user_id="test_user") mock_list.assert_called_once_with( - 1, 20, SORT_FIELD_CREATED_AT, SORT_ORDER_DESC, "test_user", team_id=None, status_filter=None + 1, + 20, + SORT_FIELD_CREATED_AT, + SORT_ORDER_DESC, + "test_user", + team_id=None, + status_filter=None, + assignee_ids=None, ) @patch("todo.services.task_service.TaskRepository.count") @@ -446,7 +502,14 @@ def test_get_tasks_explicit_sort_by_priority(self, mock_list, mock_count): TaskService.get_tasks(page=1, limit=20, sort_by=SORT_FIELD_PRIORITY, order=SORT_ORDER_DESC, user_id="test_user") mock_list.assert_called_once_with( - 1, 20, SORT_FIELD_PRIORITY, SORT_ORDER_DESC, "test_user", team_id=None, status_filter=None + 1, + 20, + SORT_FIELD_PRIORITY, + SORT_ORDER_DESC, + "test_user", + team_id=None, + status_filter=None, + assignee_ids=None, ) @patch("todo.services.task_service.TaskRepository.count") @@ -458,7 +521,14 @@ def test_get_tasks_sort_by_due_at_default_order(self, mock_list, mock_count): TaskService.get_tasks(page=1, limit=20, sort_by=SORT_FIELD_DUE_AT, order="asc", user_id="test_user") mock_list.assert_called_once_with( - 1, 20, SORT_FIELD_DUE_AT, SORT_ORDER_ASC, "test_user", team_id=None, status_filter=None + 1, + 20, + SORT_FIELD_DUE_AT, + SORT_ORDER_ASC, + "test_user", + team_id=None, + status_filter=None, + assignee_ids=None, ) @patch("todo.services.task_service.TaskRepository.count") @@ -470,7 +540,14 @@ def test_get_tasks_sort_by_priority_default_order(self, mock_list, mock_count): TaskService.get_tasks(page=1, limit=20, sort_by=SORT_FIELD_PRIORITY, order="desc", user_id="test_user") mock_list.assert_called_once_with( - 1, 20, SORT_FIELD_PRIORITY, SORT_ORDER_DESC, "test_user", team_id=None, status_filter=None + 1, + 20, + SORT_FIELD_PRIORITY, + SORT_ORDER_DESC, + "test_user", + team_id=None, + status_filter=None, + assignee_ids=None, ) @patch("todo.services.task_service.TaskRepository.count") @@ -482,7 +559,14 @@ def test_get_tasks_sort_by_assignee_default_order(self, mock_list, mock_count): TaskService.get_tasks(page=1, limit=20, sort_by=SORT_FIELD_ASSIGNEE, order="asc", user_id="test_user") mock_list.assert_called_once_with( - 1, 20, SORT_FIELD_ASSIGNEE, SORT_ORDER_ASC, "test_user", team_id=None, status_filter=None + 1, + 20, + SORT_FIELD_ASSIGNEE, + SORT_ORDER_ASC, + "test_user", + team_id=None, + status_filter=None, + assignee_ids=None, ) @patch("todo.services.task_service.TaskRepository.count") @@ -494,7 +578,14 @@ def test_get_tasks_sort_by_created_at_default_order(self, mock_list, mock_count) TaskService.get_tasks(page=1, limit=20, sort_by=SORT_FIELD_CREATED_AT, order="desc", user_id="test_user") mock_list.assert_called_once_with( - 1, 20, SORT_FIELD_CREATED_AT, SORT_ORDER_DESC, "test_user", team_id=None, status_filter=None + 1, + 20, + SORT_FIELD_CREATED_AT, + SORT_ORDER_DESC, + "test_user", + team_id=None, + status_filter=None, + assignee_ids=None, ) @patch("todo.services.task_service.reverse_lazy", return_value="/v1/tasks") diff --git a/todo/tests/unit/views/test_task.py b/todo/tests/unit/views/test_task.py index db780734..5a812ea2 100644 --- a/todo/tests/unit/views/test_task.py +++ b/todo/tests/unit/views/test_task.py @@ -53,6 +53,7 @@ def test_get_tasks_returns_200_for_valid_params(self, mock_get_tasks: Mock): user_id=str(self.user_id), team_id=None, status_filter=None, + assignee_ids=None, ) self.assertEqual(response.status_code, status.HTTP_200_OK) expected_response = mock_get_tasks.return_value.model_dump(mode="json") @@ -72,6 +73,7 @@ def test_get_tasks_returns_200_without_params(self, mock_get_tasks: Mock): user_id=str(self.user_id), team_id=None, status_filter=None, + assignee_ids=None, ) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -101,6 +103,49 @@ def test_get_tasks_returns_400_for_invalid_query_params(self): self.assertEqual(actual_error["source"]["parameter"], expected_error["source"]["parameter"]) self.assertEqual(actual_error["detail"], expected_error["detail"]) + def test_get_tasks_requires_team_for_assignee_filter(self): + response = self.client.get(self.url, {"assigneeId": str(ObjectId())}) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + error_sources = [error["source"].get("parameter") for error in response.data.get("errors", [])] + self.assertIn("teamId", error_sources) + + @patch("todo.views.task.UserTeamDetailsRepository.get_users_by_team_id", return_value=["user1"]) + def test_get_tasks_rejects_assignee_not_in_team(self, mock_get_team_members: Mock): + team_id = str(ObjectId()) + assignee_id = str(ObjectId()) + + response = self.client.get(self.url, {"teamId": team_id, "assigneeId": assignee_id}) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("errors", response.data) + self.assertTrue( + any(ValidationErrors.USER_NOT_TEAM_MEMBER in error.get("detail", "") for error in response.data["errors"]) + ) + mock_get_team_members.assert_called_once_with(team_id) + + @patch("todo.views.task.UserTeamDetailsRepository.get_users_by_team_id", return_value=["user1", "user2"]) + @patch("todo.services.task_service.TaskService.get_tasks") + def test_get_tasks_with_assignee_filter_passes_ids(self, mock_get_tasks: Mock, mock_get_team_members: Mock): + mock_get_tasks.return_value = GetTasksResponse(tasks=task_dtos) + team_id = str(ObjectId()) + params = {"teamId": team_id, "page": 1, "limit": 10, "assigneeId": ["user1", "user2"]} + + response = self.client.get(self.url, params) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + mock_get_team_members.assert_called_once_with(team_id) + mock_get_tasks.assert_called_once_with( + page=1, + limit=10, + sort_by="updatedAt", + order="desc", + user_id=str(self.user_id), + team_id=team_id, + status_filter=None, + assignee_ids=["user1", "user2"], + ) + @patch("todo.services.task_service.TaskService.get_task_by_id") def test_get_single_task_success(self, mock_get_task_by_id: Mock): valid_task_id = str(ObjectId()) @@ -183,6 +228,7 @@ def test_get_tasks_with_default_pagination(self, mock_get_tasks): user_id=str(self.user_id), team_id=None, status_filter=None, + assignee_ids=None, ) @patch("todo.services.task_service.TaskService.get_tasks") @@ -201,6 +247,7 @@ def test_get_tasks_with_valid_pagination(self, mock_get_tasks): user_id=str(self.user_id), team_id=None, status_filter=None, + assignee_ids=None, ) def test_get_tasks_with_invalid_page(self): @@ -249,6 +296,7 @@ def test_get_tasks_with_sort_by_priority(self, mock_get_tasks): user_id=str(self.user_id), team_id=None, status_filter=None, + assignee_ids=None, ) @patch("todo.services.task_service.TaskService.get_tasks") @@ -266,6 +314,7 @@ def test_get_tasks_with_sort_by_and_order(self, mock_get_tasks): user_id=str(self.user_id), team_id=None, status_filter=None, + assignee_ids=None, ) @patch("todo.services.task_service.TaskService.get_tasks") @@ -295,6 +344,7 @@ def test_get_tasks_with_all_sort_fields(self, mock_get_tasks): user_id=str(self.user_id), team_id=None, status_filter=None, + assignee_ids=None, ) @patch("todo.services.task_service.TaskService.get_tasks") @@ -318,6 +368,7 @@ def test_get_tasks_with_all_order_values(self, mock_get_tasks): user_id=str(self.user_id), team_id=None, status_filter=None, + assignee_ids=None, ) def test_get_tasks_with_invalid_sort_by(self): @@ -353,6 +404,7 @@ def test_get_tasks_sorting_with_pagination(self, mock_get_tasks): user_id=str(self.user_id), team_id=None, status_filter=None, + assignee_ids=None, ) @patch("todo.services.task_service.TaskService.get_tasks") @@ -370,6 +422,7 @@ def test_get_tasks_default_behavior_unchanged(self, mock_get_tasks): user_id=str(self.user_id), team_id=None, status_filter=None, + assignee_ids=None, ) def test_get_tasks_edge_case_combinations(self): @@ -387,6 +440,7 @@ def test_get_tasks_edge_case_combinations(self): user_id=str(self.user_id), team_id=None, status_filter=None, + assignee_ids=None, ) From a99df74b35cceb524e3f311ad474e50dbff4a5fc Mon Sep 17 00:00:00 2001 From: Achintya-Chatterjee Date: Wed, 10 Dec 2025 01:12:09 +0530 Subject: [PATCH 2/7] test(tasks): cover team + assignee filtering scenarios - add serializer, view, service, and repository specs for multi-assignee queries - exercise team-scoped assignee resolution and list/count symmetry - protect regressions where team filters should bypass user-level fallbacks --- .../test_task_sorting_integration.py | 54 ++++++++++++++++--- .../integration/test_tasks_pagination.py | 2 + 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/todo/tests/integration/test_task_sorting_integration.py b/todo/tests/integration/test_task_sorting_integration.py index 2f431f72..175d60fb 100644 --- a/todo/tests/integration/test_task_sorting_integration.py +++ b/todo/tests/integration/test_task_sorting_integration.py @@ -26,7 +26,14 @@ def test_priority_sorting_integration(self, mock_list, mock_count): self.assertEqual(response.status_code, status.HTTP_200_OK) mock_list.assert_called_with( - 1, 20, SORT_FIELD_PRIORITY, SORT_ORDER_DESC, str(self.user_id), team_id=None, status_filter=None + 1, + 20, + SORT_FIELD_PRIORITY, + SORT_ORDER_DESC, + str(self.user_id), + team_id=None, + status_filter=None, + assignee_ids=None, ) @patch("todo.repositories.task_repository.TaskRepository.count") @@ -40,7 +47,14 @@ def test_due_at_default_order_integration(self, mock_list, mock_count): self.assertEqual(response.status_code, status.HTTP_200_OK) mock_list.assert_called_with( - 1, 20, SORT_FIELD_DUE_AT, SORT_ORDER_ASC, str(self.user_id), team_id=None, status_filter=None + 1, + 20, + SORT_FIELD_DUE_AT, + SORT_ORDER_ASC, + str(self.user_id), + team_id=None, + status_filter=None, + assignee_ids=None, ) @patch("todo.repositories.task_repository.TaskRepository.count") @@ -55,7 +69,14 @@ def test_assignee_sorting_uses_aggregation(self, mock_list, mock_count): # Assignee sorting now falls back to createdAt sorting mock_list.assert_called_once_with( - 1, 20, SORT_FIELD_ASSIGNEE, SORT_ORDER_ASC, str(self.user_id), team_id=None, status_filter=None + 1, + 20, + SORT_FIELD_ASSIGNEE, + SORT_ORDER_ASC, + str(self.user_id), + team_id=None, + status_filter=None, + assignee_ids=None, ) @patch("todo.repositories.task_repository.TaskRepository.count") @@ -81,7 +102,14 @@ def test_field_specific_defaults_integration(self, mock_list, mock_count): self.assertEqual(response.status_code, status.HTTP_200_OK) mock_list.assert_called_with( - 1, 20, sort_field, expected_order, str(self.user_id), team_id=None, status_filter=None + 1, + 20, + sort_field, + expected_order, + str(self.user_id), + team_id=None, + status_filter=None, + assignee_ids=None, ) @patch("todo.repositories.task_repository.TaskRepository.count") @@ -95,7 +123,14 @@ def test_pagination_with_sorting_integration(self, mock_list, mock_count): self.assertEqual(response.status_code, status.HTTP_200_OK) mock_list.assert_called_with( - 3, 5, SORT_FIELD_CREATED_AT, SORT_ORDER_ASC, str(self.user_id), team_id=None, status_filter=None + 3, + 5, + SORT_FIELD_CREATED_AT, + SORT_ORDER_ASC, + str(self.user_id), + team_id=None, + status_filter=None, + assignee_ids=None, ) def test_invalid_sort_parameters_integration(self): @@ -116,7 +151,14 @@ def test_default_behavior_integration(self, mock_list, mock_count): self.assertEqual(response.status_code, status.HTTP_200_OK) mock_list.assert_called_with( - 1, 20, SORT_FIELD_UPDATED_AT, SORT_ORDER_DESC, str(self.user_id), team_id=None, status_filter=None + 1, + 20, + SORT_FIELD_UPDATED_AT, + SORT_ORDER_DESC, + str(self.user_id), + team_id=None, + status_filter=None, + assignee_ids=None, ) @patch("todo.repositories.user_repository.UserRepository.get_by_id") diff --git a/todo/tests/integration/test_tasks_pagination.py b/todo/tests/integration/test_tasks_pagination.py index dc7d22c9..64dde292 100644 --- a/todo/tests/integration/test_tasks_pagination.py +++ b/todo/tests/integration/test_tasks_pagination.py @@ -28,6 +28,7 @@ def test_pagination_settings_integration(self, mock_get_tasks): user_id=str(self.user_id), team_id=None, status_filter=None, + assignee_ids=None, ) mock_get_tasks.reset_mock() @@ -43,6 +44,7 @@ def test_pagination_settings_integration(self, mock_get_tasks): user_id=str(self.user_id), team_id=None, status_filter=None, + assignee_ids=None, ) # Verify API rejects values above max limit From cc273799fa4feb3c077476f09d3282ccc618eb6b Mon Sep 17 00:00:00 2001 From: Achintya-Chatterjee Date: Wed, 10 Dec 2025 01:17:07 +0530 Subject: [PATCH 3/7] test(tasks): cover team + assignee filtering scenarios - add serializer, view, service, and repository specs for multi-assignee queries - exercise team-scoped assignee resolution and list/count symmetry - protect regressions where team filters should bypass user-level fallbacks --- todo/tests/unit/services/test_task_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/todo/tests/unit/services/test_task_service.py b/todo/tests/unit/services/test_task_service.py index 49185758..2ba31e6c 100644 --- a/todo/tests/unit/services/test_task_service.py +++ b/todo/tests/unit/services/test_task_service.py @@ -132,9 +132,10 @@ def test_get_tasks_returns_empty_response_if_no_tasks_present(self, mock_list: M ) mock_count.assert_called_once_with("test_user", team_id=None, status_filter=None, assignee_ids=None) + @patch("todo.services.task_service.TeamRepository.is_user_team_member", return_value=True) @patch("todo.services.task_service.TaskRepository.count") @patch("todo.services.task_service.TaskRepository.list") - def test_get_tasks_passes_assignee_ids_to_repo(self, mock_list: Mock, mock_count: Mock): + def test_get_tasks_passes_assignee_ids_to_repo(self, mock_list: Mock, mock_count: Mock, mock_team_member: Mock): mock_list.return_value = [] mock_count.return_value = 0 From b1944d0c9b5773206947fb06377b56c95fee4715 Mon Sep 17 00:00:00 2001 From: Achintya-Chatterjee Date: Wed, 10 Dec 2025 22:24:23 +0530 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20By=20stubbing=20is=5Fuser=5Fteam=5Fm?= =?UTF-8?q?ember=20before=20asserting=20the=20repository=20calls,=20the=20?= =?UTF-8?q?test=20no=20longer=20bails=20out=20with=20a=20=E2=80=9CFORBIDDE?= =?UTF-8?q?N=E2=80=9D=20response;=20the=20mocked=20list/count=20methods=20?= =?UTF-8?q?are=20hit,=20and=20the=20assertions=20succeed.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todo/tests/unit/services/test_task_service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/todo/tests/unit/services/test_task_service.py b/todo/tests/unit/services/test_task_service.py index 2ba31e6c..edc746cb 100644 --- a/todo/tests/unit/services/test_task_service.py +++ b/todo/tests/unit/services/test_task_service.py @@ -133,6 +133,7 @@ def test_get_tasks_returns_empty_response_if_no_tasks_present(self, mock_list: M mock_count.assert_called_once_with("test_user", team_id=None, status_filter=None, assignee_ids=None) @patch("todo.services.task_service.TeamRepository.is_user_team_member", return_value=True) + @patch("todo.repositories.team_repository.TeamRepository.is_user_team_member", return_value=True) @patch("todo.services.task_service.TaskRepository.count") @patch("todo.services.task_service.TaskRepository.list") def test_get_tasks_passes_assignee_ids_to_repo(self, mock_list: Mock, mock_count: Mock, mock_team_member: Mock): From 378db29330836ec34c154ed219a1e46a4ee206ab Mon Sep 17 00:00:00 2001 From: Achintya-Chatterjee Date: Wed, 10 Dec 2025 22:39:21 +0530 Subject: [PATCH 5/7] fix:failing test --- todo/tests/unit/services/test_task_service.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/todo/tests/unit/services/test_task_service.py b/todo/tests/unit/services/test_task_service.py index edc746cb..e8fb635f 100644 --- a/todo/tests/unit/services/test_task_service.py +++ b/todo/tests/unit/services/test_task_service.py @@ -132,11 +132,12 @@ def test_get_tasks_returns_empty_response_if_no_tasks_present(self, mock_list: M ) mock_count.assert_called_once_with("test_user", team_id=None, status_filter=None, assignee_ids=None) - @patch("todo.services.task_service.TeamRepository.is_user_team_member", return_value=True) @patch("todo.repositories.team_repository.TeamRepository.is_user_team_member", return_value=True) @patch("todo.services.task_service.TaskRepository.count") @patch("todo.services.task_service.TaskRepository.list") - def test_get_tasks_passes_assignee_ids_to_repo(self, mock_list: Mock, mock_count: Mock, mock_team_member: Mock): + def test_get_tasks_passes_assignee_ids_to_repo( + self, mock_list: Mock, mock_count: Mock, mock_team_member: Mock + ): mock_list.return_value = [] mock_count.return_value = 0 From 22f49182c96b4d0954927bc18f4a018a4f36c1b2 Mon Sep 17 00:00:00 2001 From: Achintya-Chatterjee Date: Wed, 10 Dec 2025 22:41:04 +0530 Subject: [PATCH 6/7] fix: formatting --- todo/tests/unit/services/test_task_service.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/todo/tests/unit/services/test_task_service.py b/todo/tests/unit/services/test_task_service.py index e8fb635f..6b97257f 100644 --- a/todo/tests/unit/services/test_task_service.py +++ b/todo/tests/unit/services/test_task_service.py @@ -135,9 +135,7 @@ def test_get_tasks_returns_empty_response_if_no_tasks_present(self, mock_list: M @patch("todo.repositories.team_repository.TeamRepository.is_user_team_member", return_value=True) @patch("todo.services.task_service.TaskRepository.count") @patch("todo.services.task_service.TaskRepository.list") - def test_get_tasks_passes_assignee_ids_to_repo( - self, mock_list: Mock, mock_count: Mock, mock_team_member: Mock - ): + def test_get_tasks_passes_assignee_ids_to_repo(self, mock_list: Mock, mock_count: Mock, mock_team_member: Mock): mock_list.return_value = [] mock_count.return_value = 0 From 6afa99d8a4c6bfdfbab27645290deec0b9222ef4 Mon Sep 17 00:00:00 2001 From: Achintya-Chatterjee Date: Sat, 13 Dec 2025 02:18:18 +0530 Subject: [PATCH 7/7] test(tasks): update assignee filtering tests for objectid validation - use valid ObjectId strings in serializer and view tests - ensure team membership mock aligns with validated assignee ids --- .../unit/serializers/test_get_tasks_serializer.py | 12 ++++++++---- todo/tests/unit/views/test_task.py | 9 +++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/todo/tests/unit/serializers/test_get_tasks_serializer.py b/todo/tests/unit/serializers/test_get_tasks_serializer.py index 9afbce3f..d1748e51 100644 --- a/todo/tests/unit/serializers/test_get_tasks_serializer.py +++ b/todo/tests/unit/serializers/test_get_tasks_serializer.py @@ -2,6 +2,7 @@ from django.conf import settings from django.http import QueryDict from rest_framework.exceptions import ValidationError +from bson import ObjectId from todo.serializers.get_tasks_serializer import GetTaskQueryParamsSerializer from todo.constants.task import ( @@ -74,19 +75,22 @@ def test_serializer_ignores_undefined_extra_fields(self): def test_serializer_collects_assignee_ids_from_querydict(self): query_params = QueryDict(mutable=True) - query_params.setlist("assigneeId", ["user1", "user2"]) + assignee_ids = [str(ObjectId()), str(ObjectId())] + query_params.setlist("assigneeId", assignee_ids) serializer = GetTaskQueryParamsSerializer(data=query_params) self.assertTrue(serializer.is_valid()) - self.assertEqual(serializer.validated_data["assignee_ids"], ["user1", "user2"]) + self.assertEqual(serializer.validated_data["assignee_ids"], assignee_ids) def test_serializer_deduplicates_assignee_ids(self): query_params = QueryDict(mutable=True) - query_params.setlist("assigneeId", ["user1", "user1", "user2"]) + first_id = str(ObjectId()) + second_id = str(ObjectId()) + query_params.setlist("assigneeId", [first_id, first_id, second_id]) serializer = GetTaskQueryParamsSerializer(data=query_params) self.assertTrue(serializer.is_valid()) - self.assertEqual(serializer.validated_data["assignee_ids"], ["user1", "user2"]) + self.assertEqual(serializer.validated_data["assignee_ids"], [first_id, second_id]) def test_serializer_uses_django_settings_values(self): """Test that the serializer correctly uses values from Django settings""" diff --git a/todo/tests/unit/views/test_task.py b/todo/tests/unit/views/test_task.py index 5a812ea2..94f41c01 100644 --- a/todo/tests/unit/views/test_task.py +++ b/todo/tests/unit/views/test_task.py @@ -124,14 +124,15 @@ def test_get_tasks_rejects_assignee_not_in_team(self, mock_get_team_members: Moc ) mock_get_team_members.assert_called_once_with(team_id) - @patch("todo.views.task.UserTeamDetailsRepository.get_users_by_team_id", return_value=["user1", "user2"]) + @patch("todo.views.task.UserTeamDetailsRepository.get_users_by_team_id") @patch("todo.services.task_service.TaskService.get_tasks") def test_get_tasks_with_assignee_filter_passes_ids(self, mock_get_tasks: Mock, mock_get_team_members: Mock): mock_get_tasks.return_value = GetTasksResponse(tasks=task_dtos) team_id = str(ObjectId()) - params = {"teamId": team_id, "page": 1, "limit": 10, "assigneeId": ["user1", "user2"]} + assignee_ids = [str(ObjectId()), str(ObjectId())] + mock_get_team_members.return_value = assignee_ids - response = self.client.get(self.url, params) + response = self.client.get(self.url, {"teamId": team_id, "page": 1, "limit": 10, "assigneeId": assignee_ids}) self.assertEqual(response.status_code, status.HTTP_200_OK) mock_get_team_members.assert_called_once_with(team_id) @@ -143,7 +144,7 @@ def test_get_tasks_with_assignee_filter_passes_ids(self, mock_get_tasks: Mock, m user_id=str(self.user_id), team_id=team_id, status_filter=None, - assignee_ids=["user1", "user2"], + assignee_ids=assignee_ids, ) @patch("todo.services.task_service.TaskService.get_task_by_id")