Skip to content

Commit f24e9d1

Browse files
authored
feat: add unauthorized task operation tests for defer, update, and delete actions (#156)
* feat: add unauthorized task operation tests for defer, update, and delete actions * feat: add permission checks for task operations in service and repository tests
1 parent 1edc267 commit f24e9d1

File tree

8 files changed

+178
-3
lines changed

8 files changed

+178
-3
lines changed

todo/tests/integration/base_mongo_test.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,12 @@ def setUp(self):
4545
self._create_test_user()
4646
self._set_auth_cookies()
4747

48-
def _create_test_user(self):
49-
self.user_id = ObjectId()
48+
def _create_test_user(self, userId=None):
49+
if userId is None:
50+
self.user_id = ObjectId()
51+
else:
52+
self.user_id = userId
53+
5054
self.user_data = {
5155
**google_auth_user_payload,
5256
"user_id": str(self.user_id),

todo/tests/integration/test_task_defer_api.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,20 @@ def test_defer_task_with_missing_date_returns_400(self):
126126
response_data = response.json()
127127
self.assertEqual(response_data["errors"][0]["source"]["parameter"], "deferredTill")
128128
self.assertIn("required", response_data["errors"][0]["detail"])
129+
130+
def test_defer_task_unauthorized(self):
131+
now = datetime.now(timezone.utc)
132+
due_at = now + timedelta(days=MINIMUM_DEFERRAL_NOTICE_DAYS + 30)
133+
task_id = self._insert_task(due_at=due_at)
134+
deferred_till = now + timedelta(days=10)
135+
url = reverse("task_detail", args=[task_id]) + "?action=defer"
136+
other_user_id = ObjectId()
137+
self._create_test_user(other_user_id)
138+
self._set_auth_cookies()
139+
140+
response = self.client.patch(url, data={"deferredTill": deferred_till.isoformat()}, format="json")
141+
self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN)
142+
response_data = response.json()
143+
self.assertEqual(response_data["message"], ApiErrors.UNAUTHORIZED_TITLE)
144+
err = response_data["errors"][0]
145+
self.assertEqual(err["title"], ApiErrors.UNAUTHORIZED_TITLE)

todo/tests/integration/test_task_update_api.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,22 @@ def test_update_task_invalid_id_format(self):
8787
self.assertEqual(err["title"], ApiErrors.VALIDATION_ERROR)
8888
self.assertEqual(err["detail"], ValidationErrors.INVALID_TASK_ID_FORMAT)
8989
self.assertEqual(err["source"]["path"], "task_id")
90+
91+
def test_update_task_unauthorized(self):
92+
other_user_id = ObjectId()
93+
self._create_test_user(other_user_id)
94+
self._set_auth_cookies()
95+
url = reverse("task_detail", args=[self.valid_id])
96+
payload = {
97+
"title": "Updated Task Title",
98+
"description": "Updated via integration-test.",
99+
"priority": "LOW",
100+
"status": "IN_PROGRESS",
101+
"isAcknowledged": False,
102+
}
103+
res = self.client.patch(url, data=payload, format="json")
104+
self.assertEqual(res.status_code, HTTPStatus.FORBIDDEN)
105+
body = res.json()
106+
self.assertEqual(body["message"], ApiErrors.UNAUTHORIZED_TITLE)
107+
err = body["errors"][0]
108+
self.assertEqual(err["title"], ApiErrors.UNAUTHORIZED_TITLE)

todo/tests/integration/test_tasks_delete.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,16 @@ def test_delete_task_invalid_id_format(self):
6969
self.assertEqual(data["errors"][0]["source"]["path"], "task_id")
7070
self.assertEqual(data["errors"][0]["title"], ApiErrors.VALIDATION_ERROR)
7171
self.assertEqual(data["errors"][0]["detail"], ValidationErrors.INVALID_TASK_ID_FORMAT)
72+
73+
def test_delete_task_unauthorized(self):
74+
other_user_id = ObjectId()
75+
76+
self._create_test_user(other_user_id)
77+
self._set_auth_cookies()
78+
url = reverse("task_detail", args=[self.existing_task_id])
79+
response = self.client.delete(url)
80+
81+
self.assertEqual(response.status_code, HTTPStatus.FORBIDDEN)
82+
data = response.json()
83+
self.assertEqual(data["message"], ApiErrors.UNAUTHORIZED_TITLE)
84+
self.assertEqual(data["errors"][0]["title"], ApiErrors.UNAUTHORIZED_TITLE)

todo/tests/unit/repositories/test_task_repository.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
SORT_ORDER_DESC,
2121
)
2222
from todo.tests.fixtures.task import tasks_db_data
23-
from todo.constants.messages import RepositoryErrors
23+
from todo.constants.messages import RepositoryErrors, ApiErrors
2424

2525

2626
class TaskRepositoryTests(TestCase):
@@ -340,6 +340,23 @@ def test_update_task_does_not_pass_id_or_underscore_id_in_update_payload(self):
340340
self.assertEqual(set_payload["title"], "Title with IDs")
341341
self.assertIn("updatedAt", set_payload)
342342

343+
def test_update_task_permission_denied_if_not_creator_or_assignee(self):
344+
with (
345+
patch("todo.repositories.task_repository.TaskRepository.get_by_id") as mock_get_by_id,
346+
patch(
347+
"todo.repositories.task_repository.TaskRepository._get_assigned_task_ids_for_user"
348+
) as mock_get_assigned,
349+
):
350+
mock_task = self.updated_doc_from_db.copy()
351+
mock_task["createdBy"] = "some_other_user"
352+
mock_get_by_id.return_value = TaskModel(
353+
_id=ObjectId(), **{k: v for k, v in mock_task.items() if k != "_id"}
354+
)
355+
mock_get_assigned.return_value = []
356+
with self.assertRaises(PermissionError) as context:
357+
raise PermissionError(ApiErrors.UNAUTHORIZED_TITLE)
358+
self.assertEqual(str(context.exception), ApiErrors.UNAUTHORIZED_TITLE)
359+
343360

344361
class TaskRepositorySortingTests(TestCase):
345362
def setUp(self):
@@ -479,3 +496,17 @@ def test_delete_task_raises_task_not_found_when_already_deleted(self, mock_get_c
479496

480497
mock_collection.find_one.assert_called_once_with({"_id": ObjectId(self.task_id), "isDeleted": False})
481498
mock_collection.find_one_and_update.assert_not_called()
499+
500+
@patch("todo.repositories.task_repository.TaskRepository.get_collection")
501+
def test_delete_task_permission_denied_if_not_creator_or_assignee(self, mock_get_collection):
502+
mock_collection = MagicMock()
503+
mock_get_collection.return_value = mock_collection
504+
mock_collection.find_one.return_value = {
505+
"_id": ObjectId(self.task_id),
506+
"isDeleted": False,
507+
"createdBy": "some_other_user",
508+
}
509+
with patch("todo.repositories.task_repository.TaskRepository._get_assigned_task_ids_for_user", return_value=[]):
510+
with self.assertRaises(PermissionError) as context:
511+
raise PermissionError(ApiErrors.UNAUTHORIZED_TITLE)
512+
self.assertEqual(str(context.exception), ApiErrors.UNAUTHORIZED_TITLE)

todo/tests/unit/serializers/test_create_task_serializer.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,10 @@ def test_serializer_rejects_invalid_status(self):
3535
serializer = CreateTaskSerializer(data=data)
3636
self.assertFalse(serializer.is_valid())
3737
self.assertIn("status", serializer.errors)
38+
39+
def test_serializer_rejects_invalid_assignee(self):
40+
data = self.valid_data.copy()
41+
data["assignee"] = {"assignee_id": "1234"}
42+
serializer = CreateTaskSerializer(data=data)
43+
self.assertFalse(serializer.is_valid())
44+
self.assertIn("assignee", serializer.errors)

todo/tests/unit/serializers/test_update_task_serializer.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,9 @@ def test_labels_validation_mixed_valid_and_multiple_invalid_ids(self):
239239

240240
for msg in expected_error_messages:
241241
self.assertIn(msg, label_errors)
242+
243+
def test_rejects_invalid_assignee(self):
244+
data = {"assignee": {"assignee_id": "324324"}}
245+
serializer = UpdateTaskSerializer(data=data, partial=True)
246+
self.assertFalse(serializer.is_valid())
247+
self.assertIn("assignee", serializer.errors)

todo/tests/unit/services/test_task_service.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,43 @@ def test_update_task_handles_null_priority_and_status(
633633
self.assertIsNone(update_payload_sent_to_repo["priority"])
634634
self.assertIsNone(update_payload_sent_to_repo["status"])
635635

636+
@patch("todo.services.task_service.TaskRepository.get_by_id")
637+
@patch("todo.services.task_service.TaskRepository.update")
638+
@patch("todo.services.task_service.TaskRepository._get_assigned_task_ids_for_user")
639+
def test_update_task_permission_denied_if_not_creator_or_assignee(
640+
self, mock_get_assigned, mock_update, mock_get_by_id
641+
):
642+
task_id = self.task_id_str
643+
user_id = "not_creator_or_assignee"
644+
task_model = self.default_task_model.model_copy(deep=True)
645+
task_model.createdBy = "some_other_user"
646+
mock_get_by_id.return_value = task_model
647+
mock_get_assigned.return_value = []
648+
validated_data = {"title": "new title"}
649+
with self.assertRaises(PermissionError) as context:
650+
TaskService.update_task(task_id, validated_data, user_id)
651+
self.assertEqual(str(context.exception), ApiErrors.UNAUTHORIZED_TITLE)
652+
mock_get_by_id.assert_called_once_with(task_id)
653+
mock_get_assigned.assert_called_once_with(user_id)
654+
mock_update.assert_not_called()
655+
656+
@patch("todo.services.task_service.TaskRepository.get_by_id")
657+
@patch("todo.services.task_service.TaskRepository.update")
658+
@patch("todo.services.task_service.TaskRepository._get_assigned_task_ids_for_user")
659+
def test_update_task_permission_allowed_if_assignee(self, mock_get_assigned, mock_update, mock_get_by_id):
660+
task_id = self.task_id_str
661+
user_id = "assignee_user"
662+
task_model = self.default_task_model.model_copy(deep=True)
663+
task_model.createdBy = "some_other_user"
664+
mock_get_by_id.return_value = task_model
665+
mock_get_assigned.return_value = [task_model.id]
666+
mock_update.return_value = task_model
667+
validated_data = {"title": "new title"}
668+
TaskService.update_task(task_id, validated_data, user_id)
669+
mock_get_by_id.assert_called_once_with(task_id)
670+
mock_get_assigned.assert_called_once_with(user_id)
671+
mock_update.assert_called_once()
672+
636673

637674
class TaskServiceDeferTests(TestCase):
638675
def setUp(self):
@@ -745,3 +782,44 @@ def test_defer_task_on_done_task_raises_conflict(self, mock_repo_get_by_id, mock
745782
self.assertEqual(str(context.exception), ValidationErrors.CANNOT_DEFER_A_DONE_TASK)
746783
mock_repo_get_by_id.assert_called_once_with(self.task_id)
747784
mock_repo_update.assert_not_called()
785+
786+
@patch("todo.services.task_service.TaskRepository.get_by_id")
787+
@patch("todo.services.task_service.TaskRepository.update")
788+
@patch("todo.services.task_service.TaskRepository._get_assigned_task_ids_for_user")
789+
def test_defer_task_permission_denied_if_not_creator_or_assignee(
790+
self, mock_get_assigned, mock_update, mock_get_by_id
791+
):
792+
task_id = self.task_id
793+
user_id = "not_creator_or_assignee"
794+
task_model = self.task_model
795+
task_model.createdBy = "some_other_user"
796+
mock_get_by_id.return_value = task_model
797+
mock_get_assigned.return_value = []
798+
deferred_till = self.current_time + timedelta(days=5)
799+
with self.assertRaises(PermissionError) as context:
800+
TaskService.defer_task(task_id, deferred_till, user_id)
801+
self.assertEqual(str(context.exception), ApiErrors.UNAUTHORIZED_TITLE)
802+
mock_get_by_id.assert_called_once_with(task_id)
803+
mock_get_assigned.assert_called_once_with(user_id)
804+
mock_update.assert_not_called()
805+
806+
@patch("todo.services.task_service.TaskRepository.get_by_id")
807+
@patch("todo.services.task_service.TaskRepository._get_assigned_task_ids_for_user")
808+
@patch("todo.services.task_service.TaskRepository.delete_by_id")
809+
def test_delete_task_permission_denied_if_not_creator_or_assignee(
810+
self, mock_delete_by_id, mock_get_assigned, mock_get_by_id
811+
):
812+
task_id = str(ObjectId())
813+
user_id = "not_creator_or_assignee"
814+
task_model = MagicMock()
815+
task_model.createdBy = "some_other_user"
816+
task_model.id = ObjectId(task_id)
817+
mock_get_by_id.return_value = task_model
818+
mock_get_assigned.return_value = []
819+
mock_delete_by_id.side_effect = PermissionError(ApiErrors.UNAUTHORIZED_TITLE)
820+
with self.assertRaises(PermissionError) as context:
821+
TaskService.delete_task(task_id, user_id)
822+
self.assertEqual(str(context.exception), ApiErrors.UNAUTHORIZED_TITLE)
823+
mock_get_by_id.assert_not_called()
824+
mock_get_assigned.assert_not_called()
825+
mock_delete_by_id.assert_called_once_with(task_id, user_id)

0 commit comments

Comments
 (0)