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
54 changes: 48 additions & 6 deletions todo/tests/integration/test_task_sorting_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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")
Expand All @@ -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):
Expand All @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions todo/tests/integration/test_tasks_pagination.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
193 changes: 193 additions & 0 deletions todo/tests/unit/repositories/test_task_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
23 changes: 22 additions & 1 deletion todo/tests/unit/serializers/test_get_tasks_serializer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
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 bson import ObjectId

from todo.serializers.get_tasks_serializer import GetTaskQueryParamsSerializer
from todo.constants.task import (
Expand Down Expand Up @@ -71,6 +73,25 @@ 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)
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"], assignee_ids)

def test_serializer_deduplicates_assignee_ids(self):
query_params = QueryDict(mutable=True)
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"], [first_id, second_id])

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
Expand Down
Loading