snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:14:58 +05:00
commit 23059a1de3
5252 changed files with 573834 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.

View File

@@ -0,0 +1,69 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
"""
Regression tests for the ``dispatch()`` exception handling on the shared
``BaseAPIView`` / ``BaseViewSet`` classes.
When ``super().dispatch()`` raises an unhandled exception, ``dispatch()`` must
return the HTTP ``Response`` produced by ``handle_exception()`` -- not the raw
exception object. Returning the exception causes Django's response pipeline to
fail with ``TypeError: 'Exception' object is not a valid HTTP response``.
See: https://github.com/makeplane/plane/issues/9157
"""
import pytest
from unittest.mock import patch
from rest_framework import status
from rest_framework.response import Response
from rest_framework.test import APIRequestFactory
from plane.api.views.base import BaseAPIView as ApiBaseAPIView, BaseViewSet as ApiBaseViewSet
from plane.app.views.base import BaseAPIView as AppBaseAPIView, BaseViewSet as AppBaseViewSet
from plane.license.api.views.base import BaseAPIView as LicenseBaseAPIView
from plane.space.views.base import BaseAPIView as SpaceBaseAPIView, BaseViewSet as SpaceBaseViewSet
# Every shared base view that wraps ``super().dispatch()`` in a try/except.
VIEW_CLASSES = [
ApiBaseAPIView,
ApiBaseViewSet,
AppBaseAPIView,
AppBaseViewSet,
LicenseBaseAPIView,
SpaceBaseAPIView,
SpaceBaseViewSet,
]
@pytest.mark.unit
@pytest.mark.parametrize(
"view_class",
VIEW_CLASSES,
ids=lambda c: f"{c.__module__}.{c.__name__}",
)
def test_dispatch_returns_response_when_super_dispatch_raises(view_class):
"""dispatch() must return handle_exception()'s Response, not the exception."""
request = APIRequestFactory().get("/api/test/")
view = view_class()
sentinel = Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
with (
patch("rest_framework.views.APIView.dispatch", side_effect=RuntimeError("boom")),
patch.object(view_class, "handle_exception", return_value=sentinel) as mock_handle,
):
result = view.dispatch(request)
mock_handle.assert_called_once()
assert isinstance(result, Response), (
f"{view_class.__module__}.{view_class.__name__}.dispatch() returned "
f"{type(result).__name__} instead of an HTTP Response"
)
assert result is sentinel

View File

@@ -0,0 +1,212 @@
# Copyright (c) 2023-present Plane Software, Inc. and contributors
# SPDX-License-Identifier: AGPL-3.0-only
# See the LICENSE file for details.
"""
Unit regression tests for IssueLinkViewSet.partial_update.
Verifies that crawl_work_item_link_title is only scheduled when the URL
actually changes, preventing overwriting custom link metadata and unnecessary crawls.
See: https://github.com/makeplane/plane/issues/9674
"""
import uuid
from unittest.mock import MagicMock, patch
import pytest
from rest_framework import status
from rest_framework.parsers import JSONParser
from rest_framework.request import Request
from rest_framework.test import APIRequestFactory
from plane.app.views.issue.link import IssueLinkViewSet
from plane.api.views.issue import IssueLinkDetailAPIEndpoint
@pytest.mark.unit
class TestIssueLinkPartialUpdate:
@pytest.fixture(autouse=True)
def setup_view(self):
self.factory = APIRequestFactory()
self.view = IssueLinkViewSet()
self.workspace_slug = "test-workspace"
self.project_id = str(uuid.uuid4())
self.issue_id = str(uuid.uuid4())
self.link_id = str(uuid.uuid4())
self.user_id = str(uuid.uuid4())
def _create_mock_request(self, data):
wsgi_request = self.factory.patch("/api/test/", data=data, format="json")
request = Request(wsgi_request, parsers=[JSONParser()])
request.user = MagicMock()
request.user.id = self.user_id
return request
@patch("plane.app.views.issue.link.base_host", return_value="https://app.plane.so")
@patch("plane.app.views.issue.link.issue_activity")
@patch("plane.app.views.issue.link.crawl_work_item_link_title")
@patch("plane.app.views.issue.link.IssueLinkSerializer")
@patch("plane.app.views.issue.link.IssueLink.objects.get")
def test_partial_update_does_not_recrawl_when_url_unchanged(
self, mock_get_link, mock_serializer_cls, mock_crawl, mock_activity, mock_base_host
):
"""Updating link title or metadata without changing url must NOT trigger crawler."""
initial_url = "https://github.com/makeplane/plane"
mock_instance = MagicMock()
mock_instance.id = self.link_id
mock_instance.url = initial_url
mock_get_link.return_value = mock_instance
# Mock serializer behavior
mock_serializer_instance = MagicMock()
mock_serializer_instance.is_valid.return_value = True
mock_serializer_instance.data = {
"id": self.link_id,
"title": "Custom Renamed Title",
"url": initial_url,
"metadata": {"custom": "data"},
}
mock_serializer_cls.return_value = mock_serializer_instance
# Mock get_queryset for self.get_queryset().get(id=...)
mock_queryset = MagicMock()
mock_queryset.get.return_value = mock_instance
self.view.get_queryset = MagicMock(return_value=mock_queryset)
request = self._create_mock_request({"title": "Custom Renamed Title"})
response = self.view.partial_update(
request,
slug=self.workspace_slug,
project_id=self.project_id,
issue_id=self.issue_id,
pk=self.link_id,
)
assert response.status_code == status.HTTP_200_OK
mock_crawl.delay.assert_not_called()
mock_activity.delay.assert_called_once()
@patch("plane.app.views.issue.link.base_host", return_value="https://app.plane.so")
@patch("plane.app.views.issue.link.issue_activity")
@patch("plane.app.views.issue.link.crawl_work_item_link_title")
@patch("plane.app.views.issue.link.IssueLinkSerializer")
@patch("plane.app.views.issue.link.IssueLink.objects.get")
def test_partial_update_triggers_crawl_when_url_changed(
self, mock_get_link, mock_serializer_cls, mock_crawl, mock_activity, mock_base_host
):
"""Updating url to a new value MUST trigger crawl_work_item_link_title."""
initial_url = "https://github.com/makeplane/plane"
new_url = "https://github.com/makeplane/plane/pull/123"
mock_instance = MagicMock()
mock_instance.id = self.link_id
mock_instance.url = initial_url
mock_get_link.return_value = mock_instance
mock_serializer_instance = MagicMock()
mock_serializer_instance.is_valid.return_value = True
mock_serializer_instance.data = {
"id": self.link_id,
"title": "Old Title",
"url": new_url,
"metadata": {},
}
mock_serializer_cls.return_value = mock_serializer_instance
mock_queryset = MagicMock()
mock_queryset.get.return_value = mock_instance
self.view.get_queryset = MagicMock(return_value=mock_queryset)
request = self._create_mock_request({"url": new_url})
response = self.view.partial_update(
request,
slug=self.workspace_slug,
project_id=self.project_id,
issue_id=self.issue_id,
pk=self.link_id,
)
assert response.status_code == status.HTTP_200_OK
mock_crawl.delay.assert_called_once_with(self.link_id, new_url)
mock_activity.delay.assert_called_once()
@pytest.mark.unit
class TestIssueLinkDetailPatch:
@pytest.fixture(autouse=True)
def setup_view(self):
self.factory = APIRequestFactory()
self.view = IssueLinkDetailAPIEndpoint()
self.workspace_slug = "test-workspace"
self.project_id = str(uuid.uuid4())
self.issue_id = str(uuid.uuid4())
self.link_id = str(uuid.uuid4())
self.user_id = str(uuid.uuid4())
def _create_mock_request(self, data):
wsgi_request = self.factory.patch("/api/test/", data=data, format="json")
request = Request(wsgi_request, parsers=[JSONParser()])
request.user = MagicMock()
request.user.id = self.user_id
return request
@patch("plane.api.views.issue.issue_activity")
@patch("plane.api.views.issue.crawl_work_item_link_title")
@patch("plane.api.views.issue.IssueLinkSerializer")
@patch("plane.api.views.issue.IssueLink.objects.get")
def test_patch_does_not_recrawl_when_url_unchanged(
self, mock_get_link, mock_serializer_cls, mock_crawl, mock_activity
):
initial_url = "https://github.com/makeplane/plane"
mock_instance = MagicMock()
mock_instance.id = self.link_id
mock_instance.url = initial_url
mock_get_link.return_value = mock_instance
mock_serializer_instance = MagicMock()
mock_serializer_instance.is_valid.return_value = True
mock_serializer_instance.data = {"id": self.link_id, "url": initial_url}
mock_serializer_cls.return_value = mock_serializer_instance
response = self.view.patch(
self._create_mock_request({"title": "Custom Renamed Title"}),
slug=self.workspace_slug,
project_id=self.project_id,
issue_id=self.issue_id,
pk=self.link_id,
)
assert response.status_code == status.HTTP_200_OK
mock_crawl.delay.assert_not_called()
mock_activity.delay.assert_called_once()
@patch("plane.api.views.issue.issue_activity")
@patch("plane.api.views.issue.crawl_work_item_link_title")
@patch("plane.api.views.issue.IssueLinkSerializer")
@patch("plane.api.views.issue.IssueLink.objects.get")
def test_patch_triggers_crawl_when_url_changed(
self, mock_get_link, mock_serializer_cls, mock_crawl, mock_activity
):
initial_url = "https://github.com/makeplane/plane"
new_url = "https://github.com/makeplane/plane/pull/123"
mock_instance = MagicMock()
mock_instance.id = self.link_id
mock_instance.url = initial_url
mock_get_link.return_value = mock_instance
mock_serializer_instance = MagicMock()
mock_serializer_instance.is_valid.return_value = True
mock_serializer_instance.data = {"id": self.link_id, "url": new_url}
mock_serializer_cls.return_value = mock_serializer_instance
response = self.view.patch(
self._create_mock_request({"url": new_url}),
slug=self.workspace_slug,
project_id=self.project_id,
issue_id=self.issue_id,
pk=self.link_id,
)
assert response.status_code == status.HTTP_200_OK
mock_crawl.delay.assert_called_once_with(self.link_id, new_url)
mock_activity.delay.assert_called_once()