test(backend): convert existing app suites to unittest

This commit is contained in:
2026-04-30 12:41:54 +03:30
parent 204225dd16
commit 8774a4d4dc
16 changed files with 1785 additions and 1780 deletions

View File

@@ -1,8 +1,7 @@
from datetime import timedelta
import pytest
from django.utils import timezone
from rest_framework.test import APIClient
from rest_framework.test import APITestCase
from apps.clients.models import Client
from apps.projects.models import Project
@@ -11,326 +10,378 @@ from apps.users.models import User
from apps.workspaces.models import Workspace, WorkspaceMembership
@pytest.fixture()
def api_client():
return APIClient()
class WorkspaceCapabilityTests(APITestCase):
@classmethod
def setUpTestData(cls):
cls.owner = cls._user(1)
cls.admin = cls._user(2)
cls.member = cls._user(3)
cls.guest = cls._user(4)
cls.extra_owner = cls._user(5)
cls.workspace = Workspace.objects.create(name="Ops", description="", owner=cls.owner)
WorkspaceMembership.objects.create(
workspace=cls.workspace,
user=cls.admin,
role=WorkspaceMembership.Role.ADMIN,
is_active=True,
)
WorkspaceMembership.objects.create(
workspace=cls.workspace,
user=cls.member,
role=WorkspaceMembership.Role.MEMBER,
is_active=True,
)
WorkspaceMembership.objects.create(
workspace=cls.workspace,
user=cls.guest,
role=WorkspaceMembership.Role.GUEST,
is_active=True,
)
cls.project = Project.objects.create(
workspace=cls.workspace,
name="Alpha",
description="",
)
def _user(index: int) -> User:
return User.objects.create_user(
mobile=f"091255500{index:02d}",
password="secret123",
first_name=f"User{index}",
)
@staticmethod
def _user(index):
return User.objects.create_user(
mobile=f"091255500{index:02d}",
password="secret123",
first_name=f"User{index}",
)
def test_member_is_read_only_for_clients_and_projects(self):
client = Client.objects.create(
workspace=self.workspace,
name="Existing Client",
notes="",
)
self.client.force_authenticate(user=self.member)
@pytest.fixture()
def owner(db):
return _user(1)
client_response = self.client.post(
"/api/clients/",
{
"workspace_id": str(self.workspace.id),
"name": "Acme",
"notes": "",
},
format="json",
)
update_client_response = self.client.patch(
f"/api/clients/{client.id}/",
{"name": "Updated"},
format="json",
)
delete_client_response = self.client.delete(f"/api/clients/{client.id}/")
project_response = self.client.post(
"/api/projects/",
{
"workspace": str(self.workspace.id),
"name": "Beta",
"description": "",
"client": None,
},
format="json",
)
update_project_response = self.client.patch(
f"/api/projects/{self.project.id}/",
{"description": "Blocked edit"},
format="json",
)
archive_project_response = self.client.post(
f"/api/projects/{self.project.id}/archive/"
)
delete_project_response = self.client.delete(f"/api/projects/{self.project.id}/")
self.assertEqual(client_response.status_code, 403)
self.assertEqual(update_client_response.status_code, 403)
self.assertEqual(delete_client_response.status_code, 403)
self.assertEqual(project_response.status_code, 403)
self.assertEqual(update_project_response.status_code, 403)
self.assertEqual(archive_project_response.status_code, 403)
self.assertEqual(delete_project_response.status_code, 403)
@pytest.fixture()
def admin(db):
return _user(2)
def test_member_can_create_tags_and_manage_own_time_entries(self):
tag = Tag.objects.create(
workspace=self.workspace,
name="Existing",
color="#000000",
)
self.client.force_authenticate(user=self.member)
create_tag_response = self.client.post(
"/api/tags/",
{
"workspace_id": str(self.workspace.id),
"name": "New Tag",
"color": "#ffffff",
},
format="json",
)
update_tag_response = self.client.patch(
f"/api/tags/{tag.id}/",
{"name": "Changed"},
format="json",
)
delete_tag_response = self.client.delete(f"/api/tags/{tag.id}/")
@pytest.fixture()
def member(db):
return _user(3)
now = timezone.now()
create_entry_response = self.client.post(
"/api/time-entries/",
{
"workspace_id": str(self.workspace.id),
"start_time": now.isoformat(),
"end_time": (now + timedelta(hours=1)).isoformat(),
"description": "Focus block",
},
format="json",
)
self.assertEqual(create_tag_response.status_code, 201)
self.assertEqual(update_tag_response.status_code, 403)
self.assertEqual(delete_tag_response.status_code, 403)
self.assertEqual(create_entry_response.status_code, 201)
@pytest.fixture()
def guest(db):
return _user(4)
entry_id = create_entry_response.data["id"]
update_entry_response = self.client.patch(
f"/api/time-entries/{entry_id}/",
{"description": "Updated focus block"},
format="json",
)
delete_entry_response = self.client.delete(f"/api/time-entries/{entry_id}/")
self.assertEqual(update_entry_response.status_code, 200)
self.assertEqual(delete_entry_response.status_code, 204)
@pytest.fixture()
def extra_owner(db):
return _user(5)
def test_guest_is_read_only_for_workspace_resources(self):
Client.objects.create(workspace=self.workspace, name="Visible Client", notes="")
Tag.objects.create(workspace=self.workspace, name="Visible Tag", color="#123456")
self.client.force_authenticate(user=self.guest)
@pytest.fixture()
def workspace(owner, admin, member, guest):
workspace = Workspace.objects.create(name="Ops", description="", owner=owner)
WorkspaceMembership.objects.create(
workspace=workspace,
user=admin,
role=WorkspaceMembership.Role.ADMIN,
is_active=True,
)
WorkspaceMembership.objects.create(
workspace=workspace,
user=member,
role=WorkspaceMembership.Role.MEMBER,
is_active=True,
)
WorkspaceMembership.objects.create(
workspace=workspace,
user=guest,
role=WorkspaceMembership.Role.GUEST,
is_active=True,
)
return workspace
list_clients_response = self.client.get(
f"/api/clients/?workspace={self.workspace.id}"
)
list_projects_response = self.client.get(
f"/api/projects/?workspace={self.workspace.id}"
)
create_tag_response = self.client.post(
"/api/tags/",
{
"workspace_id": str(self.workspace.id),
"name": "Blocked",
"color": "#ffffff",
},
format="json",
)
create_entry_response = self.client.post(
"/api/time-entries/",
{
"workspace_id": str(self.workspace.id),
"start_time": timezone.now().isoformat(),
"description": "Blocked guest entry",
},
format="json",
)
edit_project_response = self.client.patch(
f"/api/projects/{self.project.id}/",
{"description": "Blocked"},
format="json",
)
self.assertEqual(list_clients_response.status_code, 200)
self.assertEqual(list_projects_response.status_code, 200)
self.assertEqual(create_tag_response.status_code, 403)
self.assertEqual(create_entry_response.status_code, 403)
self.assertEqual(edit_project_response.status_code, 403)
@pytest.fixture()
def project(workspace, owner, member):
return Project.objects.create(workspace=workspace, name="Alpha", description="")
def test_member_cannot_edit_project(self):
self.client.force_authenticate(user=self.member)
response = self.client.patch(
f"/api/projects/{self.project.id}/",
{"description": "Still blocked"},
format="json",
)
def test_member_is_read_only_for_clients_and_projects(api_client, member, workspace, project):
client = Client.objects.create(workspace=workspace, name="Existing Client", notes="")
api_client.force_authenticate(user=member)
self.assertEqual(response.status_code, 403)
client_response = api_client.post(
"/api/clients/",
{"workspace_id": str(workspace.id), "name": "Acme", "notes": ""},
format="json",
)
update_client_response = api_client.patch(
f"/api/clients/{client.id}/",
{"name": "Updated"},
format="json",
)
delete_client_response = api_client.delete(f"/api/clients/{client.id}/")
project_response = api_client.post(
"/api/projects/",
{"workspace": str(workspace.id), "name": "Beta", "description": "", "client": None},
format="json",
)
update_project_response = api_client.patch(
f"/api/projects/{project.id}/",
{"description": "Blocked edit"},
format="json",
)
archive_project_response = api_client.post(f"/api/projects/{project.id}/archive/")
delete_project_response = api_client.delete(f"/api/projects/{project.id}/")
assert client_response.status_code == 403
assert update_client_response.status_code == 403
assert delete_client_response.status_code == 403
assert project_response.status_code == 403
assert update_project_response.status_code == 403
assert archive_project_response.status_code == 403
assert delete_project_response.status_code == 403
def test_member_can_list_workspace_members_with_restricted_user_fields(self):
self.client.force_authenticate(user=self.member)
response = self.client.get(
f"/api/workspace-memberships/?workspace={self.workspace.id}"
)
def test_member_can_create_tags_and_manage_own_time_entries(api_client, owner, member, workspace):
tag = Tag.objects.create(workspace=workspace, name="Existing", color="#000000")
api_client.force_authenticate(user=member)
self.assertEqual(response.status_code, 200)
payload = (
response.data.get("items", response.data)
if isinstance(response.data, dict)
else response.data
)
self.assertGreaterEqual(len(payload), 1)
first_user = payload[0]["user"]
self.assertNotIn("mobile", first_user)
self.assertNotIn("email", first_user)
create_tag_response = api_client.post(
"/api/tags/",
{"workspace_id": str(workspace.id), "name": "New Tag", "color": "#ffffff"},
format="json",
)
update_tag_response = api_client.patch(
f"/api/tags/{tag.id}/",
{"name": "Changed"},
format="json",
)
delete_tag_response = api_client.delete(f"/api/tags/{tag.id}/")
def test_owner_can_list_workspace_members_with_full_user_fields(self):
self.client.force_authenticate(user=self.owner)
now = timezone.now()
create_entry_response = api_client.post(
"/api/time-entries/",
{
"workspace_id": str(workspace.id),
"start_time": now.isoformat(),
"end_time": (now + timedelta(hours=1)).isoformat(),
"description": "Focus block",
},
format="json",
)
response = self.client.get(
f"/api/workspace-memberships/?workspace={self.workspace.id}"
)
assert create_tag_response.status_code == 201
assert update_tag_response.status_code == 403
assert delete_tag_response.status_code == 403
assert create_entry_response.status_code == 201
self.assertEqual(response.status_code, 200)
payload = (
response.data.get("items", response.data)
if isinstance(response.data, dict)
else response.data
)
self.assertGreaterEqual(len(payload), 1)
first_user = payload[0]["user"]
self.assertIn("mobile", first_user)
entry_id = create_entry_response.data["id"]
update_entry_response = api_client.patch(
f"/api/time-entries/{entry_id}/",
{"description": "Updated focus block"},
format="json",
)
delete_entry_response = api_client.delete(f"/api/time-entries/{entry_id}/")
def test_admin_cannot_change_owner_membership_but_canonical_owner_can(self):
extra_owner_membership = WorkspaceMembership.objects.create(
workspace=self.workspace,
user=self.extra_owner,
role=WorkspaceMembership.Role.OWNER,
is_active=True,
)
assert update_entry_response.status_code == 200
assert delete_entry_response.status_code == 204
self.client.force_authenticate(user=self.admin)
admin_response = self.client.patch(
f"/api/workspace-memberships/{extra_owner_membership.id}/",
{"role": WorkspaceMembership.Role.ADMIN},
format="json",
)
self.client.force_authenticate(user=self.owner)
owner_response = self.client.patch(
f"/api/workspace-memberships/{extra_owner_membership.id}/",
{"role": WorkspaceMembership.Role.ADMIN},
format="json",
)
def test_guest_is_read_only_for_workspace_resources(api_client, owner, guest, workspace, project):
Client.objects.create(workspace=workspace, name="Visible Client", notes="")
Tag.objects.create(workspace=workspace, name="Visible Tag", color="#123456")
self.assertEqual(admin_response.status_code, 403)
self.assertEqual(owner_response.status_code, 200)
api_client.force_authenticate(user=guest)
def test_admin_cannot_add_or_change_admin_memberships(self):
admin_membership = WorkspaceMembership.objects.get(
workspace=self.workspace,
user=self.admin,
is_deleted=False,
)
list_clients_response = api_client.get(f"/api/clients/?workspace={workspace.id}")
list_projects_response = api_client.get(f"/api/projects/?workspace={workspace.id}")
create_tag_response = api_client.post(
"/api/tags/",
{"workspace_id": str(workspace.id), "name": "Blocked", "color": "#ffffff"},
format="json",
)
create_entry_response = api_client.post(
"/api/time-entries/",
{
"workspace_id": str(workspace.id),
"start_time": timezone.now().isoformat(),
"description": "Blocked guest entry",
},
format="json",
)
edit_project_response = api_client.patch(
f"/api/projects/{project.id}/",
{"description": "Blocked"},
format="json",
)
self.client.force_authenticate(user=self.admin)
create_response = self.client.post(
"/api/workspace-memberships/",
{
"workspace": str(self.workspace.id),
"user": str(self.member.id),
"role": WorkspaceMembership.Role.ADMIN,
},
format="json",
)
update_response = self.client.patch(
f"/api/workspace-memberships/{admin_membership.id}/",
{"role": WorkspaceMembership.Role.MEMBER},
format="json",
)
delete_response = self.client.delete(
f"/api/workspace-memberships/{admin_membership.id}/"
)
assert list_clients_response.status_code == 200
assert list_projects_response.status_code == 200
assert create_tag_response.status_code == 403
assert create_entry_response.status_code == 403
assert edit_project_response.status_code == 403
self.assertEqual(create_response.status_code, 403)
self.assertEqual(update_response.status_code, 403)
self.assertEqual(delete_response.status_code, 403)
def test_admin_can_delete_only_owned_clients_tags_and_projects(self):
self.client.force_authenticate(user=self.owner)
owner_client_response = self.client.post(
"/api/clients/",
{
"workspace_id": str(self.workspace.id),
"name": "Owner Client",
"notes": "",
},
format="json",
)
owner_tag_response = self.client.post(
"/api/tags/",
{
"workspace_id": str(self.workspace.id),
"name": "Owner Tag",
"color": "#123456",
},
format="json",
)
owner_project_response = self.client.post(
"/api/projects/",
{
"workspace": str(self.workspace.id),
"name": "Owner Project",
"description": "",
"client": None,
},
format="json",
)
def test_member_cannot_edit_project(api_client, member, project):
api_client.force_authenticate(user=member)
self.client.force_authenticate(user=self.admin)
admin_client_response = self.client.post(
"/api/clients/",
{
"workspace_id": str(self.workspace.id),
"name": "Admin Client",
"notes": "",
},
format="json",
)
admin_tag_response = self.client.post(
"/api/tags/",
{
"workspace_id": str(self.workspace.id),
"name": "Admin Tag",
"color": "#654321",
},
format="json",
)
admin_project_response = self.client.post(
"/api/projects/",
{
"workspace": str(self.workspace.id),
"name": "Admin Project",
"description": "",
"client": None,
},
format="json",
)
response = api_client.patch(
f"/api/projects/{project.id}/",
{"description": "Still blocked"},
format="json",
)
delete_owner_client = self.client.delete(
f"/api/clients/{owner_client_response.data['id']}/"
)
delete_owner_tag = self.client.delete(
f"/api/tags/{owner_tag_response.data['id']}/"
)
delete_owner_project = self.client.delete(
f"/api/projects/{owner_project_response.data['id']}/"
)
assert response.status_code == 403
delete_admin_client = self.client.delete(
f"/api/clients/{admin_client_response.data['id']}/"
)
delete_admin_tag = self.client.delete(
f"/api/tags/{admin_tag_response.data['id']}/"
)
delete_admin_project = self.client.delete(
f"/api/projects/{admin_project_response.data['id']}/"
)
def test_member_can_list_workspace_members_with_restricted_user_fields(api_client, member, workspace):
api_client.force_authenticate(user=member)
response = api_client.get(f"/api/workspace-memberships/?workspace={workspace.id}")
assert response.status_code == 200
payload = response.data.get("items", response.data) if isinstance(response.data, dict) else response.data
assert len(payload) >= 1
first_user = payload[0]["user"]
assert "mobile" not in first_user
assert "email" not in first_user
def test_owner_can_list_workspace_members_with_full_user_fields(api_client, owner, workspace):
api_client.force_authenticate(user=owner)
response = api_client.get(f"/api/workspace-memberships/?workspace={workspace.id}")
assert response.status_code == 200
payload = response.data.get("items", response.data) if isinstance(response.data, dict) else response.data
assert len(payload) >= 1
first_user = payload[0]["user"]
assert "mobile" in first_user
def test_admin_cannot_change_owner_membership_but_canonical_owner_can(
api_client, owner, admin, extra_owner, workspace
):
extra_owner_membership = WorkspaceMembership.objects.create(
workspace=workspace,
user=extra_owner,
role=WorkspaceMembership.Role.OWNER,
is_active=True,
)
api_client.force_authenticate(user=admin)
admin_response = api_client.patch(
f"/api/workspace-memberships/{extra_owner_membership.id}/",
{"role": WorkspaceMembership.Role.ADMIN},
format="json",
)
api_client.force_authenticate(user=owner)
owner_response = api_client.patch(
f"/api/workspace-memberships/{extra_owner_membership.id}/",
{"role": WorkspaceMembership.Role.ADMIN},
format="json",
)
assert admin_response.status_code == 403
assert owner_response.status_code == 200
def test_admin_cannot_add_or_change_admin_memberships(api_client, owner, admin, member, workspace):
admin_membership = WorkspaceMembership.objects.get(workspace=workspace, user=admin, is_deleted=False)
api_client.force_authenticate(user=admin)
create_response = api_client.post(
"/api/workspace-memberships/",
{
"workspace": str(workspace.id),
"user": str(member.id),
"role": WorkspaceMembership.Role.ADMIN,
},
format="json",
)
update_response = api_client.patch(
f"/api/workspace-memberships/{admin_membership.id}/",
{"role": WorkspaceMembership.Role.MEMBER},
format="json",
)
delete_response = api_client.delete(f"/api/workspace-memberships/{admin_membership.id}/")
assert create_response.status_code == 403
assert update_response.status_code == 403
assert delete_response.status_code == 403
def test_admin_can_delete_only_owned_clients_tags_and_projects(api_client, owner, admin, workspace):
api_client.force_authenticate(user=owner)
owner_client_response = api_client.post(
"/api/clients/",
{"workspace_id": str(workspace.id), "name": "Owner Client", "notes": ""},
format="json",
)
owner_tag_response = api_client.post(
"/api/tags/",
{"workspace_id": str(workspace.id), "name": "Owner Tag", "color": "#123456"},
format="json",
)
owner_project_response = api_client.post(
"/api/projects/",
{"workspace": str(workspace.id), "name": "Owner Project", "description": "", "client": None},
format="json",
)
api_client.force_authenticate(user=admin)
admin_client_response = api_client.post(
"/api/clients/",
{"workspace_id": str(workspace.id), "name": "Admin Client", "notes": ""},
format="json",
)
admin_tag_response = api_client.post(
"/api/tags/",
{"workspace_id": str(workspace.id), "name": "Admin Tag", "color": "#654321"},
format="json",
)
admin_project_response = api_client.post(
"/api/projects/",
{"workspace": str(workspace.id), "name": "Admin Project", "description": "", "client": None},
format="json",
)
delete_owner_client = api_client.delete(f"/api/clients/{owner_client_response.data['id']}/")
delete_owner_tag = api_client.delete(f"/api/tags/{owner_tag_response.data['id']}/")
delete_owner_project = api_client.delete(f"/api/projects/{owner_project_response.data['id']}/")
delete_admin_client = api_client.delete(f"/api/clients/{admin_client_response.data['id']}/")
delete_admin_tag = api_client.delete(f"/api/tags/{admin_tag_response.data['id']}/")
delete_admin_project = api_client.delete(f"/api/projects/{admin_project_response.data['id']}/")
assert delete_owner_client.status_code == 403
assert delete_owner_tag.status_code == 403
assert delete_owner_project.status_code in {403, 404}
assert delete_admin_client.status_code == 204
assert delete_admin_tag.status_code == 204
assert delete_admin_project.status_code == 204
self.assertEqual(delete_owner_client.status_code, 403)
self.assertEqual(delete_owner_tag.status_code, 403)
self.assertIn(delete_owner_project.status_code, {403, 404})
self.assertEqual(delete_admin_client.status_code, 204)
self.assertEqual(delete_admin_tag.status_code, 204)
self.assertEqual(delete_admin_project.status_code, 204)