diff --git a/backend/quickstart/serializers.py b/backend/quickstart/serializers.py index 7a3fc2a..28501bd 100644 --- a/backend/quickstart/serializers.py +++ b/backend/quickstart/serializers.py @@ -49,4 +49,9 @@ def validate_due_date(self, value: datetime) -> datetime: def validate_reward(self, value: int) -> int: if value < 0: raise serializers.ValidationError("Reward cannot be negative") + + # Reward moderation will go here, for now we can have basic moderation and set a limit (like $100) + if value > 100: + raise serializers.ValidationError("Reward exceeds permitted amount") + return value diff --git a/backend/quickstart/tests.py b/backend/quickstart/tests.py index a5aa975..a6c78f3 100644 --- a/backend/quickstart/tests.py +++ b/backend/quickstart/tests.py @@ -37,12 +37,12 @@ def test_task(db: Any, test_user: StudyUser) -> Task: name="Test", reward=50, description="Make sure this works", - due_date="2026-12-31", + due_date="2026-12-31T00:00:00Z", user=test_user, ) -# @pytest.mark.required +@pytest.mark.required @pytest.mark.tasks class TestTaskCreation: def test_create_task_authenticated(self, api_client: APIClient, test_user: StudyUser) -> None: @@ -55,7 +55,7 @@ def test_create_task_authenticated(self, api_client: APIClient, test_user: Study "name": "Math Homework", "reward": 50, "description": "Finish algebra 1", - "due_date": "2029-12-31", + "due_date": "2029-12-31T00:00:00Z", } response = api_client.post(url, data, format="json") @@ -70,7 +70,7 @@ def test_create_task_unauthenticated(self, api_client: APIClient, test_user: Stu url = "/api/create_task/" data = { "name": "Ghost Task", - "due_date": "2029-12-31", + "due_date": "2029-12-31T00:00:00Z", } response = api_client.post(url, data, format="json") @@ -86,7 +86,7 @@ def test_create_task_empty( """Ensure each submitted task is not empty.""" api_client.force_authenticate(user=test_user) - response = api_client.post("/api/create_task", {}, format="json") + response = api_client.post("/api/create_task/", {}, format="json") assert response.status_code == status.HTTP_400_BAD_REQUEST @@ -95,7 +95,7 @@ def test_create_task_missing_name(self, api_client: APIClient, test_user: StudyU api_client.force_authenticate(user=test_user) data = { "reward": 10, - "due_date": "2029-12-31", + "due_date": "2029-12-31T00:00:00Z", "description": "test", } @@ -141,7 +141,7 @@ def test_create_task_negative_reward(self, api_client: APIClient, test_user: Stu data = { "name": "Negative Task", "reward": -100, - "due_date": "2029-12-31", + "due_date": "2029-12-31T00:00:00Z", } response = api_client.post("/api/create_task/", data, format="json") @@ -153,7 +153,7 @@ def test_create_task_positive_reward(self, api_client: APIClient, test_user: Stu data = { "name": "Positive Task", "reward": 100, - "due_date": "2029-12-31", + "due_date": "2029-12-31T00:00:00Z", } response = api_client.post("/api/create_task/", data, format="json") @@ -167,7 +167,7 @@ def test_create_task_due_date_in_past( data = { "name": "Late Task", "reward": 10, - "due_date": "2000-01-01", + "due_date": "2000-01-01T00:00:00Z", } response = api_client.post("/api/create_task/", data, format="json") @@ -176,7 +176,7 @@ def test_create_task_due_date_in_past( def test_create_task_response_shape(self, api_client: APIClient, test_user: StudyUser): api_client.force_authenticate(user=test_user) - data = {"name": "Shape Test", "reward": 20, "due_date": "2029-12-31"} + data = {"name": "Shape Test", "reward": 20, "due_date": "2029-12-31T00:00:00Z"} response = api_client.post("/api/create_task/", data, format="json") @@ -195,7 +195,7 @@ def test_create_task_missing_description_uses_default( data = { "name": "No Description Task", "reward": 10, - "due_date": "2029-12-31", + "due_date": "2029-12-31T00:00:00Z", } response = api_client.post("/api/create_task/", data, format="json") @@ -211,13 +211,29 @@ def test_create_task_reward_zero(self, api_client: APIClient, test_user: StudyUs data = { "name": "Zero Reward Task", "reward": 0, - "due_date": "2029-12-31", + "due_date": "2029-12-31T00:00:00Z", } response = api_client.post("/api/create_task/", data, format="json") assert response.status_code == status.HTTP_201_CREATED + def test_create_task_excessive_reward( + self, api_client: APIClient, test_user: StudyUser + ) -> None: + """A reward higher than the set limit should fail our current basic moderation""" + # The value in this test is not 101 because this limit is subject to change + api_client.force_authenticate(user=test_user) + data = { + "name": "Money Farming Task", + "reward": 10000000000, + "due_date": "2029-12-31T00:00:00Z", + } + + response = api_client.post("/api/create_task/", data, format="json") + + assert response.status_code == status.HTTP_400_BAD_REQUEST + def test_create_task_reward_as_string( self, api_client: APIClient, test_user: StudyUser ) -> None: @@ -226,7 +242,7 @@ def test_create_task_reward_as_string( data = { "name": "String Reward Task", "reward": "50", - "due_date": "2029-12-31", + "due_date": "2029-12-31T00:00:00Z", } response = api_client.post("/api/create_task/", data, format="json") @@ -241,7 +257,7 @@ def test_create_task_reward_non_numeric_string( data = { "name": "Bad Reward Task", "reward": "not-a-number", - "due_date": "2029-12-31", + "due_date": "2029-12-31T00:00:00Z", } response = api_client.post("/api/create_task/", data, format="json") @@ -259,7 +275,7 @@ def test_create_task_missing_name_uses_default( api_client.force_authenticate(user=test_user) data = { "reward": 10, - "due_date": "2029-12-31", + "due_date": "2029-12-31T00:00:00Z", } response = api_client.post("/api/create_task/", data, format="json") @@ -273,7 +289,7 @@ def test_create_task_with_category(self, api_client: APIClient, test_user: Study data = { "name": "Categorized Task", "reward": 10, - "due_date": "2029-12-31", + "due_date": "2029-12-31T00:00:00Z", "category": "school", } @@ -292,7 +308,7 @@ def test_create_task_without_category_is_null( data = { "name": "Uncategorized Task", "reward": 10, - "due_date": "2029-12-31", + "due_date": "2029-12-31T00:00:00Z", } response = api_client.post("/api/create_task/", data, format="json") @@ -307,7 +323,7 @@ def test_create_task_does_not_leak_other_users_id( data = { "name": "Spoofed User Task", "reward": 10, - "due_date": "2029-12-31", + "due_date": "2029-12-31T00:00:00Z", "user": other_user.id, } @@ -356,7 +372,7 @@ def test_get_task_returns_only_own_tasks( name="Other User Task", reward=5, description="not visible", - due_date="2029-12-31", + due_date="2029-12-31T00:00:00Z", user=other_user, ) @@ -407,9 +423,15 @@ def test_get_task_returns_multiple_tasks( """A user with several tasks should get all of them back.""" api_client.force_authenticate(user=test_user) - Task.objects.create(name="Task A", reward=10, due_date="2029-12-31", user=test_user) - Task.objects.create(name="Task B", reward=20, due_date="2029-12-31", user=test_user) - Task.objects.create(name="Task C", reward=30, due_date="2029-12-31", user=test_user) + Task.objects.create( + name="Task A", reward=10, due_date="2029-12-31T00:00:00Z", user=test_user + ) + Task.objects.create( + name="Task B", reward=20, due_date="2029-12-31T00:00:00Z", user=test_user + ) + Task.objects.create( + name="Task C", reward=30, due_date="2029-12-31T00:00:00Z", user=test_user + ) response = api_client.get("/api/get_task/") @@ -452,8 +474,12 @@ def test_get_task_does_not_return_soft_deleted_or_unrelated_data( ) -> None: """Sanity check that retrieval count matches exactly what was created — no duplication or leakage.""" api_client.force_authenticate(user=test_user) - Task.objects.create(name="Only Mine", reward=5, due_date="2029-12-31", user=test_user) - Task.objects.create(name="Not Mine", reward=5, due_date="2029-12-31", user=other_user) + Task.objects.create( + name="Only Mine", reward=5, due_date="2029-12-31T00:00:00Z", user=test_user + ) + Task.objects.create( + name="Not Mine", reward=5, due_date="2029-12-31T00:00:00Z", user=other_user + ) response = api_client.get("/api/get_task/") @@ -479,7 +505,7 @@ def test_get_task_includes_category_field( Task.objects.create( name="Categorized", reward=10, - due_date="2029-12-31", + due_date="2029-12-31T00:00:00Z", category="school", user=test_user, ) @@ -496,7 +522,7 @@ def test_get_task_field_values_are_correct_types( Task.objects.create( name="Type Check", reward=42, - due_date="2029-07-04", + due_date="2029-07-04T00:00:00Z", user=test_user, ) @@ -506,7 +532,7 @@ def test_get_task_field_values_are_correct_types( assert response.status_code == status.HTTP_200_OK task_data = response.data[0] assert task_data["reward"] == 42 - assert "2029-07-04" in task_data["due_date"] + assert "2029-07-04T00:00:00Z" in task_data["due_date"] def test_get_task_response_is_a_list( self, api_client: APIClient, test_user: StudyUser, test_task: Task @@ -529,7 +555,7 @@ def test_get_task_pagination_if_enabled( """ for i in range(25): Task.objects.create( - name=f"Bulk Task {i}", reward=1, due_date="2029-12-31", user=test_user + name=f"Bulk Task {i}", reward=1, due_date="2029-12-31T00:00:00Z", user=test_user ) api_client.force_authenticate(user=test_user) @@ -604,7 +630,7 @@ def test_isolation_holds_across_multiple_tasks_per_user( for i in range(3): api_client.post( "/api/create_task/", - {"name": f"Mine {i}", "reward": 5, "due_date": "2029-12-31"}, + {"name": f"Mine {i}", "reward": 5, "due_date": "2029-12-31T00:00:00Z"}, format="json", ) @@ -612,7 +638,7 @@ def test_isolation_holds_across_multiple_tasks_per_user( for i in range(2): api_client.post( "/api/create_task/", - {"name": f"Theirs {i}", "reward": 5, "due_date": "2029-12-31"}, + {"name": f"Theirs {i}", "reward": 5, "due_date": "2029-12-31T00:00:00Z"}, format="json", ) @@ -622,6 +648,351 @@ def test_isolation_holds_across_multiple_tasks_per_user( assert len(names) == 2 assert all(name.startswith("Theirs") for name in names) + def test_same_task_name_allowed_across_different_users( + self, api_client: APIClient, test_user: StudyUser, other_user: StudyUser + ) -> None: + """Task names aren't globally unique — two users can each have a task with the same name.""" + Task.objects.create( + name="Homework", reward=10, due_date="2029-12-31T00:00:00Z", user=test_user + ) + Task.objects.create( + name="Homework", reward=20, due_date="2029-12-31T00:00:00Z", user=other_user + ) + + assert Task.objects.filter(name="Homework").count() == 2 + mine = Task.objects.get(name="Homework", user=test_user) + theirs = Task.objects.get(name="Homework", user=other_user) + assert mine.id != theirs.id + assert mine.reward != theirs.reward + + def test_user_cannot_access_task_by_guessing_id( + self, api_client: APIClient, test_user: StudyUser, other_user: StudyUser + ) -> None: + """Sequential/guessable IDs shouldn't let one user fetch another's specific task.""" + task = Task.objects.create( + name="Secret Task", reward=10, due_date="2029-12-31T00:00:00Z", user=other_user + ) + + api_client.force_authenticate(user=test_user) + response = api_client.get("/api/get_task/", data={"id": task.id}) + + assert response.status_code in (status.HTTP_403_FORBIDDEN, status.HTTP_404_NOT_FOUND) + + def test_isolation_holds_after_switching_authenticated_user_mid_session( + self, api_client: APIClient, test_user: StudyUser, other_user: StudyUser + ) -> None: + """Re-authenticating as a different user on the same client shouldn't leak the previous user's tasks.""" + api_client.force_authenticate(user=test_user) + api_client.post( + "/api/create_task/", + {"name": "First User Task", "reward": 5, "due_date": "2029-12-31T00:00:00Z"}, + format="json", + ) + + api_client.force_authenticate(user=other_user) + response = api_client.get("/api/get_task/") + + names = [t["name"] for t in response.data] + assert "First User Task" not in names + + def test_create_task_does_not_expose_other_users_task_count( + self, api_client: APIClient, test_user: StudyUser, other_user: StudyUser + ) -> None: + """Creating a task for one user shouldn't be influenced by or leak another user's existing tasks.""" + Task.objects.create( + name="Existing", reward=10, due_date="2029-12-31T00:00:00Z", user=other_user + ) + + api_client.force_authenticate(user=test_user) + response = api_client.post( + "/api/create_task/", + {"name": "New Task", "reward": 5, "due_date": "2029-12-31T00:00:00Z"}, + format="json", + ) + + assert response.status_code == status.HTTP_201_CREATED + assert Task.objects.filter(user=test_user).count() == 1 + assert Task.objects.filter(user=other_user).count() == 1 + + def test_get_task_count_matches_only_authenticated_users_tasks( + self, api_client: APIClient, test_user: StudyUser, other_user: StudyUser + ) -> None: + """Total tasks in the DB across all users shouldn't affect what one user sees.""" + Task.objects.create( + name="Mine 1", reward=5, due_date="2029-12-31T00:00:00Z", user=test_user + ) + Task.objects.create( + name="Theirs 1", reward=5, due_date="2029-12-31T00:00:00Z", user=other_user + ) + Task.objects.create( + name="Theirs 2", reward=5, due_date="2029-12-31T00:00:00Z", user=other_user + ) + Task.objects.create( + name="Theirs 3", reward=5, due_date="2029-12-31T00:00:00Z", user=other_user + ) + + api_client.force_authenticate(user=test_user) + response = api_client.get("/api/get_task/") + + assert response.status_code == status.HTTP_200_OK + assert Task.objects.count() == 4 # confirms all 4 exist in the DB + assert len(response.data) == 1 # but only 1 is visible to test_user + + def test_creating_many_tasks_for_one_user_does_not_appear_for_another( + self, api_client: APIClient, test_user: StudyUser, other_user: StudyUser + ) -> None: + """Bulk creation for one user should never leak into another user's view.""" + api_client.force_authenticate(user=test_user) + for i in range(10): + api_client.post( + "/api/create_task/", + {"name": f"Task {i}", "reward": 1, "due_date": "2029-12-31T00:00:00Z"}, + format="json", + ) + + api_client.force_authenticate(user=other_user) + response = api_client.get("/api/get_task/") + + assert response.status_code == status.HTTP_200_OK + assert response.data == [] + + def test_task_reward_values_are_isolated_between_users( + self, api_client: APIClient, test_user: StudyUser, other_user: StudyUser + ) -> None: + """Confirm reward/money fields on tasks aren't cross-contaminated between users' tasks.""" + Task.objects.create(name="Cheap", reward=1, due_date="2029-12-31T00:00:00Z", user=test_user) + Task.objects.create( + name="Expensive", reward=1000, due_date="2029-12-31T00:00:00Z", user=other_user + ) + + api_client.force_authenticate(user=test_user) + response = api_client.get("/api/get_task/") + + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 1 + assert response.data[0]["reward"] == 1 + + def test_get_task_only_returns_own_tasks( + self, api_client: APIClient, test_user: StudyUser, other_user: StudyUser + ) -> None: + """Create tasks interleaved between two users, confirm isolation without relying on IDs.""" + api_client.force_authenticate(user=test_user) + api_client.post( + "/api/create_task/", + {"name": "A", "reward": 1, "due_date": "2029-12-31T00:00:00Z"}, + format="json", + ) + + api_client.force_authenticate(user=other_user) + api_client.post( + "/api/create_task/", + {"name": "B", "reward": 1, "due_date": "2029-12-31T00:00:00Z"}, + format="json", + ) + + api_client.force_authenticate(user=test_user) + response = api_client.get("/api/get_task/") + names = [t["name"] for t in response.data] + + assert "A" in names + assert "B" not in names + + +@pytest.mark.tasks +class TestTaskDeletion: + def test_delete_task_authenticated_owner( + self, api_client: APIClient, test_user: StudyUser, test_task: Task + ) -> None: + """The owner of a task should be able to delete it.""" + api_client.force_authenticate(user=test_user) + + response = api_client.delete(f"/api/delete_task/{test_task.id}/") + + assert response.status_code in (status.HTTP_200_OK, status.HTTP_204_NO_CONTENT) + assert not Task.objects.filter(id=test_task.id).exists() + + def test_delete_task_unauthenticated(self, api_client: APIClient, test_task: Task) -> None: + """A logged-out request should not be able to delete anything.""" + response = api_client.delete(f"/api/delete_task/{test_task.id}/") + + assert response.status_code == status.HTTP_403_FORBIDDEN + assert Task.objects.filter(id=test_task.id).exists() + + def test_delete_task_nonexistent_id(self, api_client: APIClient, test_user: StudyUser) -> None: + """Deleting an ID that doesn't exist should 404, not 500.""" + api_client.force_authenticate(user=test_user) + + response = api_client.delete("/api/delete_task/999999/") + + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_delete_task_other_users_task_forbidden( + self, api_client: APIClient, test_user: StudyUser, other_user: StudyUser + ) -> None: + """A user should not be able to delete a task they don't own.""" + task = Task.objects.create( + name="Not Yours", reward=10, due_date="2029-12-31T00:00:00Z", user=other_user + ) + + api_client.force_authenticate(user=test_user) + response = api_client.delete(f"/api/delete_task/{task.id}/") + + assert response.status_code in (status.HTTP_403_FORBIDDEN, status.HTTP_404_NOT_FOUND) + assert Task.objects.filter(id=task.id).exists() + + def test_delete_task_removes_only_target_task( + self, api_client: APIClient, test_user: StudyUser + ) -> None: + """Deleting one task shouldn't affect the user's other tasks.""" + task1 = Task.objects.create( + name="Keep", reward=5, due_date="2029-12-31T00:00:00Z", user=test_user + ) + task2 = Task.objects.create( + name="Remove", reward=5, due_date="2029-12-31T00:00:00Z", user=test_user + ) + + api_client.force_authenticate(user=test_user) + response = api_client.delete(f"/api/delete_task/{task2.id}/") + + assert response.status_code in (status.HTTP_200_OK, status.HTTP_204_NO_CONTENT) + assert Task.objects.filter(id=task1.id).exists() + assert not Task.objects.filter(id=task2.id).exists() + + def test_delete_task_invalid_id_format( + self, api_client: APIClient, test_user: StudyUser + ) -> None: + """A non-numeric ID in the URL shouldn't cause a 500.""" + api_client.force_authenticate(user=test_user) + + response = api_client.delete("/api/delete_task/not-an-id/") + + assert response.status_code in (status.HTTP_404_NOT_FOUND, status.HTTP_400_BAD_REQUEST) + + def test_delete_task_twice_second_call_fails_gracefully( + self, api_client: APIClient, test_user: StudyUser, test_task: Task + ) -> None: + """Deleting the same task twice shouldn't 500 on the second attempt.""" + api_client.force_authenticate(user=test_user) + + first = api_client.delete(f"/api/delete_task/{test_task.id}/") + second = api_client.delete(f"/api/delete_task/{test_task.id}/") + + assert first.status_code in (status.HTTP_200_OK, status.HTTP_204_NO_CONTENT) + assert second.status_code == status.HTTP_404_NOT_FOUND + + def test_delete_task_response_has_no_body_or_confirms_deletion( + self, api_client: APIClient, test_user: StudyUser, test_task: Task + ) -> None: + """If the view returns 200 with a body, confirm it doesn't leak the deleted object's user info.""" + api_client.force_authenticate(user=test_user) + + response = api_client.delete(f"/api/delete_task/{test_task.id}/") + + if response.status_code == status.HTTP_200_OK and response.data: + assert "password" not in response.data + assert "user" not in response.data or not isinstance(response.data.get("user"), dict) + + def test_delete_task_get_request_not_allowed( + self, api_client: APIClient, test_user: StudyUser, test_task: Task + ) -> None: + """A GET to the delete endpoint shouldn't accidentally delete anything.""" + api_client.force_authenticate(user=test_user) + + response = api_client.get(f"/api/delete_task/{test_task.id}/") + + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + assert Task.objects.filter(id=test_task.id).exists() + + def test_delete_task_does_not_affect_other_users_tasks( + self, api_client: APIClient, test_user: StudyUser, other_user: StudyUser + ) -> None: + """Deleting your own task shouldn't touch anyone else's.""" + my_task = Task.objects.create( + name="Mine", reward=5, due_date="2029-12-31T00:00:00Z", user=test_user + ) + their_task = Task.objects.create( + name="Theirs", reward=5, due_date="2029-12-31T00:00:00Z", user=other_user + ) + + api_client.force_authenticate(user=test_user) + api_client.delete(f"/api/delete_task/{my_task.id}/") + + assert Task.objects.filter(id=their_task.id).exists() + + def test_delete_task_does_not_delete_user( + self, api_client: APIClient, test_user: StudyUser, test_task: Task + ) -> None: + """Deleting a task should never cascade upward and delete the owning user.""" + api_client.force_authenticate(user=test_user) + + api_client.delete(f"/api/delete_task/{test_task.id}/") + + assert StudyUser.objects.filter(id=test_user.id).exists() + + def test_delete_task_negative_id(self, api_client: APIClient, test_user: StudyUser) -> None: + """A negative ID should be handled gracefully, not cause a 500.""" + api_client.force_authenticate(user=test_user) + + response = api_client.delete("/api/delete_task/-1/") + + assert response.status_code in (status.HTTP_404_NOT_FOUND, status.HTTP_400_BAD_REQUEST) + + def test_delete_task_inactive_user_cannot_delete( + self, api_client: APIClient, test_user: StudyUser, test_task: Task + ) -> None: + """A deactivated account shouldn't be able to delete tasks either.""" + test_user.is_active = False + test_user.save() + + api_client.force_authenticate(user=test_user) + response = api_client.delete(f"/api/delete_task/{test_task.id}/") + + assert response.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN) + assert Task.objects.filter(id=test_task.id).exists() + + def test_delete_task_reduces_users_task_count( + self, api_client: APIClient, test_user: StudyUser + ) -> None: + """Confirm the count of the user's remaining tasks drops by exactly one.""" + Task.objects.create(name="A", reward=1, due_date="2029-12-31T00:00:00Z", user=test_user) + task_b = Task.objects.create( + name="B", reward=1, due_date="2029-12-31T00:00:00Z", user=test_user + ) + Task.objects.create(name="C", reward=1, due_date="2029-12-31T00:00:00Z", user=test_user) + + api_client.force_authenticate(user=test_user) + api_client.delete(f"/api/delete_task/{task_b.id}/") + + assert Task.objects.filter(user=test_user).count() == 2 + + def test_delete_task_does_not_return_stale_data_on_get_after_delete( + self, api_client: APIClient, test_user: StudyUser, test_task: Task + ) -> None: + """After deleting a task, a subsequent get_task call shouldn't still show it.""" + api_client.force_authenticate(user=test_user) + + api_client.delete(f"/api/delete_task/{test_task.id}/") + response = api_client.get("/api/get_task/") + + assert response.status_code == status.HTTP_200_OK + names = [t["name"] for t in response.data] + assert test_task.name not in names + + def test_delete_task_id_belonging_to_different_task_type_or_missing_fk( + self, api_client: APIClient, test_user: StudyUser + ) -> None: + """Deleting a task whose id was already reassigned/reused after a prior deletion shouldn't 500.""" + task = Task.objects.create( + name="Temp", reward=5, due_date="2029-12-31T00:00:00Z", user=test_user + ) + deleted_id = task.id + task.delete() + + api_client.force_authenticate(user=test_user) + response = api_client.delete(f"/api/delete_task/{deleted_id}/") + + assert response.status_code == status.HTTP_404_NOT_FOUND + # Test Graveyard for tests that get generated but aren't useful *yet* @@ -631,9 +1002,67 @@ def test_isolation_holds_across_multiple_tasks_per_user( # Task.objects.create( # name="Cascade Task", # reward=10, -# due_date="2029-12-31", +# due_date="2029-12-31T00:00:00Z", # user=test_user, # ) # test_user.delete() # assert not Task.objects.filter(name="Cascade Task").exists() + +# Isolation Tests! (might need to make a second class) + +# def test_user_cannot_delete_another_users_task( +# self, api_client: APIClient, test_user: StudyUser, other_user: StudyUser +# ) -> None: +# """A user should not be able to delete a task they don't own.""" +# task = Task.objects.create( +# name="Other's Task", reward=10, due_date="2029-12-31", user=other_user +# ) + +# api_client.force_authenticate(user=test_user) +# response = api_client.delete(f"/api/delete_task/{task.id}/") + +# assert response.status_code in (status.HTTP_403_FORBIDDEN, status.HTTP_404_NOT_FOUND) +# assert Task.objects.filter(id=task.id).exists() + +# def test_user_cannot_update_another_users_task( +# self, api_client: APIClient, test_user: StudyUser, other_user: StudyUser +# ) -> None: +# """A user should not be able to modify a task they don't own.""" +# task = Task.objects.create( +# name="Original Name", reward=10, due_date="2029-12-31", user=other_user +# ) + +# api_client.force_authenticate(user=test_user) +# response = api_client.patch( +# f"/api/update_task/{task.id}/", {"name": "Hacked Name"}, format="json" +# ) + +# assert response.status_code in (status.HTTP_403_FORBIDDEN, status.HTTP_404_NOT_FOUND) +# task.refresh_from_db() +# assert task.name == "Original Name" + +# def test_deleting_one_user_does_not_affect_other_users_tasks( +# self, api_client: APIClient, test_user: StudyUser, other_user: StudyUser +# ) -> None: +# """Cascade delete should only remove the deleted user's own tasks.""" +# Task.objects.create(name="Mine", reward=10, due_date="2029-12-31", user=test_user) +# Task.objects.create(name="Theirs", reward=10, due_date="2029-12-31", user=other_user) + +# test_user.delete() + +# assert not Task.objects.filter(name="Mine").exists() +# assert Task.objects.filter(name="Theirs").exists() + +# def test_same_task_name_allowed_across_different_users( +# self, api_client: APIClient, test_user: StudyUser, other_user: StudyUser +# ) -> None: +# """Task names aren't globally unique — two users can each have a task with the same name.""" +# Task.objects.create(name="Homework", reward=10, due_date="2029-12-31", user=test_user) +# Task.objects.create(name="Homework", reward=20, due_date="2029-12-31", user=other_user) + +# assert Task.objects.filter(name="Homework").count() == 2 +# mine = Task.objects.get(name="Homework", user=test_user) +# theirs = Task.objects.get(name="Homework", user=other_user) +# assert mine.id != theirs.id +# assert mine.reward != theirs.reward