From d7db88fc13b019a720ff3207fa944f38d632fcfe Mon Sep 17 00:00:00 2001 From: Ivan Kuznetsov <1456389+wildsurfer@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:49:38 +0100 Subject: [PATCH 1/2] fix: send notifications when work items are created or updated via the REST API --- apps/api/plane/api/views/issue.py | 8 ++ .../plane/tests/contract/api/test_issues.py | 135 ++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 apps/api/plane/tests/contract/api/test_issues.py diff --git a/apps/api/plane/api/views/issue.py b/apps/api/plane/api/views/issue.py index 02fcfce6f94..dfcb76f8252 100644 --- a/apps/api/plane/api/views/issue.py +++ b/apps/api/plane/api/views/issue.py @@ -479,6 +479,8 @@ def post(self, request, slug, project_id): project_id=str(project_id), current_instance=None, epoch=int(timezone.now().timestamp()), + notification=True, + origin=base_host(request=request, is_app=True), ) # Send the model activity @@ -638,6 +640,8 @@ def put(self, request, slug, project_id): project_id=str(project_id), current_instance=current_instance, epoch=int(timezone.now().timestamp()), + notification=True, + origin=base_host(request=request, is_app=True), ) # Send the model activity for webhook dispatch model_activity.delay( @@ -696,6 +700,8 @@ def put(self, request, slug, project_id): project_id=str(project_id), current_instance=None, epoch=int(timezone.now().timestamp()), + notification=True, + origin=base_host(request=request, is_app=True), ) # Send the model activity for webhook dispatch model_activity.delay( @@ -781,6 +787,8 @@ def patch(self, request, slug, project_id, pk): project_id=str(project_id), current_instance=current_instance, epoch=int(timezone.now().timestamp()), + notification=True, + origin=base_host(request=request, is_app=True), ) # Send the model activity for webhook dispatch model_activity.delay( diff --git a/apps/api/plane/tests/contract/api/test_issues.py b/apps/api/plane/tests/contract/api/test_issues.py new file mode 100644 index 00000000000..ca2533757c4 --- /dev/null +++ b/apps/api/plane/tests/contract/api/test_issues.py @@ -0,0 +1,135 @@ +# Copyright (c) 2023-present Plane Software, Inc. and contributors +# SPDX-License-Identifier: AGPL-3.0-only +# See the LICENSE file for details. + +from unittest.mock import patch + +import pytest +from rest_framework import status + +from plane.db.models import Issue, Project, ProjectMember, State, User + + +@pytest.fixture +def project(db, workspace, create_user): + """Create a test project with the user as an admin member and a default state.""" + project = Project.objects.create( + name="Test Project", + identifier="TP", + workspace=workspace, + created_by=create_user, + ) + ProjectMember.objects.create( + project=project, + member=create_user, + role=20, # Admin role + is_active=True, + ) + # A default state is required to create work items through the API + State.objects.create( + name="Backlog", + color="#000000", + group="backlog", + default=True, + project=project, + workspace=workspace, + created_by=create_user, + ) + return project + + +@pytest.fixture +def create_issue(db, project, workspace, create_user): + """Create an existing work item to update/assign in tests.""" + return Issue.objects.create( + name="Existing Issue", + project=project, + workspace=workspace, + created_by=create_user, + ) + + +@pytest.fixture +def assignee_user(db): + """Create a second user that can be assigned to a work item.""" + user = User.objects.create( + email="assignee@plane.so", + username="assignee-user", + first_name="Assignee", + last_name="User", + ) + user.set_password("assignee-password") + user.save() + return user + + +@pytest.mark.contract +class TestIssueNotificationContract: + """ + Contract: creating and updating/assigning a work item through the external + REST API (``/api/v1/...``) must trigger notifications, i.e. ``issue_activity`` + is dispatched with ``notification=True`` so subscribers and assignees are + notified the same way the web app does. See makeplane/plane#9306. + """ + + def get_list_url(self, workspace_slug, project_id): + """Helper to build the work item list/create endpoint URL.""" + return f"/api/v1/workspaces/{workspace_slug}/projects/{project_id}/issues/" + + def get_detail_url(self, workspace_slug, project_id, issue_id): + """Helper to build the work item detail endpoint URL.""" + return f"/api/v1/workspaces/{workspace_slug}/projects/{project_id}/issues/{issue_id}/" + + @pytest.mark.django_db + def test_create_issue_triggers_notification(self, api_key_client, workspace, project): + """Creating a work item via the external API dispatches a notifying activity.""" + url = self.get_list_url(workspace.slug, project.id) + + with patch("plane.api.views.issue.issue_activity") as mock_issue_activity: + response = api_key_client.post(url, {"name": "New Issue"}, format="json") + + assert response.status_code == status.HTTP_201_CREATED + assert Issue.objects.filter(name="New Issue").exists() + + mock_issue_activity.delay.assert_called_once() + kwargs = mock_issue_activity.delay.call_args.kwargs + assert kwargs["type"] == "issue.activity.created" + assert kwargs["notification"] is True + + @pytest.mark.django_db + def test_update_issue_triggers_notification(self, api_key_client, workspace, project, create_issue): + """Updating a work item via the external API dispatches a notifying activity.""" + url = self.get_detail_url(workspace.slug, project.id, create_issue.id) + + with patch("plane.api.views.issue.issue_activity") as mock_issue_activity: + response = api_key_client.patch(url, {"name": "Renamed Issue"}, format="json") + + assert response.status_code == status.HTTP_200_OK + create_issue.refresh_from_db() + assert create_issue.name == "Renamed Issue" + + mock_issue_activity.delay.assert_called_once() + kwargs = mock_issue_activity.delay.call_args.kwargs + assert kwargs["type"] == "issue.activity.updated" + assert kwargs["notification"] is True + + @pytest.mark.django_db + def test_assign_issue_triggers_notification(self, api_key_client, workspace, project, create_issue, assignee_user): + """Assigning a work item via the external API dispatches a notifying activity.""" + ProjectMember.objects.create( + project=project, + member=assignee_user, + role=15, # Member role + is_active=True, + ) + url = self.get_detail_url(workspace.slug, project.id, create_issue.id) + + with patch("plane.api.views.issue.issue_activity") as mock_issue_activity: + response = api_key_client.patch(url, {"assignees": [str(assignee_user.id)]}, format="json") + + assert response.status_code == status.HTTP_200_OK + + mock_issue_activity.delay.assert_called_once() + kwargs = mock_issue_activity.delay.call_args.kwargs + assert kwargs["type"] == "issue.activity.updated" + assert kwargs["notification"] is True From 0755f94615913a0712b214ac6440ffa40ca0d155 Mon Sep 17 00:00:00 2001 From: Ivan Kuznetsov <1456389+wildsurfer@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:35:56 +0100 Subject: [PATCH 2/2] test: rename contract test file to test_issue_notifications.py --- .../contract/api/{test_issues.py => test_issue_notifications.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/api/plane/tests/contract/api/{test_issues.py => test_issue_notifications.py} (100%) diff --git a/apps/api/plane/tests/contract/api/test_issues.py b/apps/api/plane/tests/contract/api/test_issue_notifications.py similarity index 100% rename from apps/api/plane/tests/contract/api/test_issues.py rename to apps/api/plane/tests/contract/api/test_issue_notifications.py