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
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:
4
apps/api/plane/tests/unit/serializers/__init__.py
Normal file
4
apps/api/plane/tests/unit/serializers/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
import pytest
|
||||
|
||||
from plane.db.models import (
|
||||
Workspace,
|
||||
Project,
|
||||
Issue,
|
||||
User,
|
||||
IssueAssignee,
|
||||
WorkspaceMember,
|
||||
ProjectMember,
|
||||
)
|
||||
from plane.app.serializers.workspace import IssueRecentVisitSerializer
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestIssueRecentVisitSerializer:
|
||||
"""Test the IssueRecentVisitSerializer"""
|
||||
|
||||
def test_issue_recent_visit_serializer_fields(self, db):
|
||||
"""Test that the serializer includes the correct fields"""
|
||||
|
||||
test_user_1 = User.objects.create(email="test_user_1@example.com", first_name="Test", last_name="User")
|
||||
|
||||
# To test for deleted issue assignee
|
||||
test_user_2 = User.objects.create(
|
||||
email="test_user_2@example.com",
|
||||
first_name="Other",
|
||||
last_name="User",
|
||||
username="some user name",
|
||||
)
|
||||
|
||||
workspace = Workspace.objects.create(name="Test Workspace", slug="test-workspace", owner=test_user_1)
|
||||
|
||||
WorkspaceMember.objects.create(member=test_user_2, role=15, workspace=workspace)
|
||||
|
||||
project = Project.objects.create(name="Test Project", identifier="test-project", workspace=workspace)
|
||||
ProjectMember.objects.create(project=project, member=test_user_2)
|
||||
|
||||
issue = Issue.objects.create(
|
||||
name="Test Issue",
|
||||
workspace=workspace,
|
||||
project=project,
|
||||
)
|
||||
|
||||
IssueAssignee.objects.create(issue=issue, assignee=test_user_1, project=project)
|
||||
|
||||
# Deleted issue assignee
|
||||
IssueAssignee.objects.create(
|
||||
issue=issue,
|
||||
assignee=test_user_2,
|
||||
project=project,
|
||||
deleted_at=timezone.now(),
|
||||
)
|
||||
|
||||
serialized_data = IssueRecentVisitSerializer(
|
||||
issue,
|
||||
).data
|
||||
|
||||
# Check fields are present and correct
|
||||
assert "name" in serialized_data
|
||||
assert "assignees" in serialized_data
|
||||
assert "project_identifier" in serialized_data
|
||||
|
||||
assert serialized_data["name"] == "Test Issue"
|
||||
assert serialized_data["project_identifier"] == "TEST-PROJECT"
|
||||
|
||||
# Only including non-deleted issue assignees
|
||||
assert serialized_data["assignees"] == [test_user_1.id]
|
||||
@@ -0,0 +1,61 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
import pytest
|
||||
|
||||
from plane.api.serializers.issue import IssueSerializer
|
||||
from plane.db.models import Project, ProjectMember, Label, User
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestIssueSerializerAssigneeAndLabelValidation:
|
||||
"""Test that IssueSerializer rejects invalid assignees/labels instead of silently dropping them"""
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_rejects_assignee_who_is_not_an_active_project_member(self, db, workspace, create_user):
|
||||
"""An assignee id that isn't an active project member (role >= 15) must raise a validation error"""
|
||||
project = Project.objects.create(name="Test Project", identifier="TEST", workspace=workspace)
|
||||
|
||||
outsider = User.objects.create(
|
||||
email="outsider@example.com", first_name="Out", last_name="Sider", username="outsider"
|
||||
)
|
||||
# Not a member of the project at all
|
||||
|
||||
serializer = IssueSerializer(
|
||||
data={"name": "Test Issue", "assignees": [str(outsider.id)]},
|
||||
context={"project_id": project.id, "workspace_id": workspace.id},
|
||||
)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "assignees" in str(serializer.errors).lower() or "assignee" in str(serializer.errors).lower()
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_rejects_label_that_does_not_belong_to_project(self, db, workspace, create_user):
|
||||
"""A label id that belongs to a different project must raise a validation error"""
|
||||
project = Project.objects.create(name="Test Project", identifier="TEST", workspace=workspace)
|
||||
other_project = Project.objects.create(name="Other Project", identifier="OTHER", workspace=workspace)
|
||||
|
||||
foreign_label = Label.objects.create(name="Foreign Label", project=other_project)
|
||||
|
||||
serializer = IssueSerializer(
|
||||
data={"name": "Test Issue", "labels": [str(foreign_label.id)]},
|
||||
context={"project_id": project.id, "workspace_id": workspace.id},
|
||||
)
|
||||
|
||||
assert not serializer.is_valid()
|
||||
assert "labels" in str(serializer.errors).lower() or "label" in str(serializer.errors).lower()
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_accepts_assignee_who_is_an_active_project_member(self, db, workspace, create_user):
|
||||
"""A valid active project member id should still be accepted"""
|
||||
project = Project.objects.create(name="Test Project", identifier="TEST", workspace=workspace)
|
||||
ProjectMember.objects.create(project=project, member=create_user, role=15, is_active=True)
|
||||
|
||||
serializer = IssueSerializer(
|
||||
data={"name": "Test Issue", "assignees": [str(create_user.id)]},
|
||||
context={"project_id": project.id, "workspace_id": workspace.id},
|
||||
)
|
||||
|
||||
assert serializer.is_valid(), serializer.errors
|
||||
assert list(serializer.validated_data["assignees"]) == [create_user.id]
|
||||
41
apps/api/plane/tests/unit/serializers/test_label.py
Normal file
41
apps/api/plane/tests/unit/serializers/test_label.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
import pytest
|
||||
from plane.app.serializers import LabelSerializer
|
||||
from plane.db.models import Project, Label
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestLabelSerializer:
|
||||
"""Test the LabelSerializer"""
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_label_serializer_create_valid_data(self, db, workspace):
|
||||
"""Test creating a label with valid data"""
|
||||
project = Project.objects.create(name="Test Project", identifier="TEST", workspace=workspace)
|
||||
|
||||
serializer = LabelSerializer(
|
||||
data={"name": "Test Label"},
|
||||
context={"project_id": project.id},
|
||||
)
|
||||
assert serializer.is_valid()
|
||||
assert serializer.errors == {}
|
||||
serializer.save(project_id=project.id)
|
||||
|
||||
label = Label.objects.all().first()
|
||||
assert label.name == "Test Label"
|
||||
assert label.project == project
|
||||
assert label
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_label_serializer_create_duplicate_name(self, db, workspace):
|
||||
"""Test creating a label with a duplicate name"""
|
||||
project = Project.objects.create(name="Test Project", identifier="TEST", workspace=workspace)
|
||||
|
||||
Label.objects.create(name="Test Label", project=project)
|
||||
|
||||
serializer = LabelSerializer(data={"name": "Test Label"}, context={"project_id": project.id})
|
||||
assert not serializer.is_valid()
|
||||
assert serializer.errors == {"name": ["LABEL_NAME_ALREADY_EXISTS"]}
|
||||
129
apps/api/plane/tests/unit/serializers/test_workspace.py
Normal file
129
apps/api/plane/tests/unit/serializers/test_workspace.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# See the LICENSE file for details.
|
||||
|
||||
import pytest
|
||||
from uuid import uuid4
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from plane.api.serializers import WorkspaceLiteSerializer
|
||||
from plane.app.serializers import WorkSpaceSerializer
|
||||
from plane.license.api.serializers import WorkspaceSerializer as InstanceWorkspaceSerializer
|
||||
from plane.db.models import Workspace, User
|
||||
|
||||
# Names with no letter or digit — must be rejected (issue #9255)
|
||||
SYMBOL_ONLY_NAMES = ["-_________-", "---", "___", "- - -", " ", " -_ "]
|
||||
|
||||
# Names containing at least one letter or digit — must be accepted, including
|
||||
# international scripts, so the rule does not regress non-Latin workspace names
|
||||
VALID_NAMES = [
|
||||
"Acme Corp",
|
||||
"Acme_Corp-123",
|
||||
"123",
|
||||
"a",
|
||||
"R&D",
|
||||
"José",
|
||||
"Müller GmbH",
|
||||
"日本語",
|
||||
"株式会社",
|
||||
"محمد",
|
||||
]
|
||||
|
||||
# Names embedding a URL — must be rejected on both create paths
|
||||
URL_NAMES = ["https://evil.com", "www.example.com", "example.com"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestWorkspaceLiteSerializer:
|
||||
"""Test the WorkspaceLiteSerializer"""
|
||||
|
||||
def test_workspace_lite_serializer_fields(self, db):
|
||||
"""Test that the serializer includes the correct fields"""
|
||||
# Create a user to be the owner
|
||||
owner = User.objects.create(email="test@example.com", first_name="Test", last_name="User")
|
||||
|
||||
# Create a workspace with explicit ID to test serialization
|
||||
workspace_id = uuid4()
|
||||
workspace = Workspace.objects.create(name="Test Workspace", slug="test-workspace", id=workspace_id, owner=owner)
|
||||
|
||||
# Serialize the workspace
|
||||
serialized_data = WorkspaceLiteSerializer(workspace).data
|
||||
|
||||
# Check fields are present and correct
|
||||
assert "name" in serialized_data
|
||||
assert "slug" in serialized_data
|
||||
assert "id" in serialized_data
|
||||
|
||||
assert serialized_data["name"] == "Test Workspace"
|
||||
assert serialized_data["slug"] == "test-workspace"
|
||||
assert str(serialized_data["id"]) == str(workspace_id)
|
||||
|
||||
def test_workspace_lite_serializer_read_only(self, db):
|
||||
"""Test that the serializer fields are read-only"""
|
||||
# Create a user to be the owner
|
||||
owner = User.objects.create(email="test2@example.com", first_name="Test", last_name="User")
|
||||
|
||||
# Create a workspace
|
||||
workspace = Workspace.objects.create(name="Test Workspace", slug="test-workspace", id=uuid4(), owner=owner)
|
||||
|
||||
# Try to update via serializer
|
||||
serializer = WorkspaceLiteSerializer(workspace, data={"name": "Updated Name", "slug": "updated-slug"})
|
||||
|
||||
# Serializer should be valid (since read-only fields are ignored)
|
||||
assert serializer.is_valid()
|
||||
|
||||
# Save should not update the read-only fields
|
||||
updated_workspace = serializer.save()
|
||||
assert updated_workspace.name == "Test Workspace"
|
||||
assert updated_workspace.slug == "test-workspace"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestWorkSpaceSerializerNameValidation:
|
||||
"""validate_name must reject symbol-only workspace names (issue #9255).
|
||||
|
||||
Frontend validation is bypassable via a direct API call, so the rule is
|
||||
enforced server-side on both the create and rename (partial_update) paths,
|
||||
which share this serializer's field-level validation.
|
||||
"""
|
||||
|
||||
@pytest.mark.parametrize("name", SYMBOL_ONLY_NAMES)
|
||||
def test_rejects_symbol_only_names(self, name):
|
||||
serializer = WorkSpaceSerializer()
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
serializer.validate_name(name)
|
||||
|
||||
@pytest.mark.parametrize("name", VALID_NAMES)
|
||||
def test_accepts_names_with_alphanumeric(self, name):
|
||||
serializer = WorkSpaceSerializer()
|
||||
assert serializer.validate_name(name) == name
|
||||
|
||||
@pytest.mark.parametrize("name", URL_NAMES)
|
||||
def test_rejects_names_containing_urls(self, name):
|
||||
serializer = WorkSpaceSerializer()
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
serializer.validate_name(name)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestInstanceWorkspaceSerializerNameValidation:
|
||||
"""The instance/license workspace create path must enforce the same rules
|
||||
as the app serializer (symbol-only rejection AND URL rejection)."""
|
||||
|
||||
@pytest.mark.parametrize("name", SYMBOL_ONLY_NAMES)
|
||||
def test_rejects_symbol_only_names(self, name):
|
||||
serializer = InstanceWorkspaceSerializer()
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
serializer.validate_name(name)
|
||||
|
||||
@pytest.mark.parametrize("name", VALID_NAMES)
|
||||
def test_accepts_names_with_alphanumeric(self, name):
|
||||
serializer = InstanceWorkspaceSerializer()
|
||||
assert serializer.validate_name(name) == name
|
||||
|
||||
@pytest.mark.parametrize("name", URL_NAMES)
|
||||
def test_rejects_names_containing_urls(self, name):
|
||||
serializer = InstanceWorkspaceSerializer()
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
serializer.validate_name(name)
|
||||
Reference in New Issue
Block a user