test(backend): convert existing app suites to unittest
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -1,128 +1,184 @@
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
from django.test import TestCase
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from apps.projects.models import Project
|
||||
from apps.time_entries.services.rates import resolve_rate
|
||||
from apps.users.models import User
|
||||
from apps.workspaces.models import PriceUnit, Workspace, WorkspaceMembership, WorkspaceUserRate
|
||||
from apps.workspaces.models import (
|
||||
PriceUnit,
|
||||
Workspace,
|
||||
WorkspaceMembership,
|
||||
WorkspaceUserRate,
|
||||
)
|
||||
from apps.workspaces.services.rates import (
|
||||
update_workspace_user_rate,
|
||||
upsert_workspace_user_rate,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def api_client():
|
||||
return APIClient()
|
||||
class WorkspaceRateTests(APITestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.owner = User.objects.create_user(mobile="09127770001", password="secret123")
|
||||
cls.admin = User.objects.create_user(mobile="09127770002", password="secret123")
|
||||
cls.member = User.objects.create_user(mobile="09127770003", password="secret123")
|
||||
|
||||
cls.workspace = Workspace.objects.create(name="Rates", 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,
|
||||
)
|
||||
cls.project = Project.objects.create(workspace=cls.workspace, name="Billing")
|
||||
|
||||
PriceUnit.objects.create(
|
||||
code="USD",
|
||||
name="US Dollar",
|
||||
local_name="Dollar",
|
||||
symbol="$",
|
||||
)
|
||||
PriceUnit.objects.create(
|
||||
code="EUR",
|
||||
name="Euro",
|
||||
local_name="Euro",
|
||||
symbol="EUR",
|
||||
)
|
||||
|
||||
def test_resolve_rate_uses_workspace_user_rate(self):
|
||||
WorkspaceUserRate.objects.create(
|
||||
workspace=self.workspace,
|
||||
user=self.member,
|
||||
hourly_rate=Decimal("40.00"),
|
||||
currency="EUR",
|
||||
effective_from=self.project.created_at,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
hourly_rate, currency = resolve_rate(self.member, self.project)
|
||||
|
||||
self.assertEqual(hourly_rate, Decimal("40.00"))
|
||||
self.assertEqual(currency, "EUR")
|
||||
|
||||
def test_resolve_rate_returns_none_when_workspace_rate_is_missing(self):
|
||||
hourly_rate, currency = resolve_rate(self.member, self.project)
|
||||
|
||||
self.assertIsNone(hourly_rate)
|
||||
self.assertEqual(currency, "")
|
||||
|
||||
def test_admin_can_manage_workspace_user_rates(self):
|
||||
self.client.force_authenticate(user=self.admin)
|
||||
|
||||
create_response = self.client.post(
|
||||
"/api/workspace-user-rates/",
|
||||
{
|
||||
"workspace_id": str(self.workspace.id),
|
||||
"user_id": str(self.member.id),
|
||||
"hourly_rate": "35.50",
|
||||
"currency": "USD",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(create_response.status_code, 201)
|
||||
rate_id = create_response.data["id"]
|
||||
self.assertTrue(
|
||||
WorkspaceUserRate.objects.filter(id=rate_id, is_deleted=False).exists()
|
||||
)
|
||||
|
||||
update_response = self.client.patch(
|
||||
f"/api/workspace-user-rates/{rate_id}/",
|
||||
{"hourly_rate": "42.00"},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(update_response.status_code, 200)
|
||||
self.assertEqual(update_response.data["hourly_rate"], "42.00")
|
||||
|
||||
delete_response = self.client.delete(f"/api/workspace-user-rates/{rate_id}/")
|
||||
self.assertEqual(delete_response.status_code, 204)
|
||||
self.assertTrue(WorkspaceUserRate.all_objects.get(id=rate_id).is_deleted)
|
||||
|
||||
def test_member_cannot_manage_rates(self):
|
||||
self.client.force_authenticate(user=self.member)
|
||||
|
||||
response = self.client.post(
|
||||
"/api/workspace-user-rates/",
|
||||
{
|
||||
"workspace_id": str(self.workspace.id),
|
||||
"user_id": str(self.member.id),
|
||||
"hourly_rate": "25.00",
|
||||
"currency": "USD",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def owner(db):
|
||||
return User.objects.create_user(mobile="09127770001", password="secret123")
|
||||
class WorkspaceRateServiceTests(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.owner = User.objects.create_user(mobile="09127770011", password="secret123")
|
||||
cls.member = User.objects.create_user(mobile="09127770012", password="secret123")
|
||||
cls.workspace = Workspace.objects.create(name="Rate Services", owner=cls.owner)
|
||||
|
||||
def test_upsert_workspace_user_rate_creates_uppercase_currency_rate(self):
|
||||
rate = upsert_workspace_user_rate(
|
||||
self.workspace,
|
||||
self.member.id,
|
||||
Decimal("12.50"),
|
||||
"usd",
|
||||
)
|
||||
|
||||
@pytest.fixture()
|
||||
def admin(db):
|
||||
return User.objects.create_user(mobile="09127770002", password="secret123")
|
||||
self.assertEqual(rate.hourly_rate, Decimal("12.50"))
|
||||
self.assertEqual(rate.currency, "USD")
|
||||
self.assertTrue(rate.is_active)
|
||||
|
||||
def test_upsert_workspace_user_rate_updates_existing_inactive_rate(self):
|
||||
rate = WorkspaceUserRate.objects.create(
|
||||
workspace=self.workspace,
|
||||
user=self.member,
|
||||
hourly_rate=Decimal("10.00"),
|
||||
currency="USD",
|
||||
effective_from=self.workspace.created_at,
|
||||
is_active=False,
|
||||
)
|
||||
|
||||
@pytest.fixture()
|
||||
def member(db):
|
||||
return User.objects.create_user(mobile="09127770003", password="secret123")
|
||||
updated = upsert_workspace_user_rate(
|
||||
self.workspace,
|
||||
self.member.id,
|
||||
Decimal("20.00"),
|
||||
"eur",
|
||||
)
|
||||
|
||||
self.assertEqual(updated.id, rate.id)
|
||||
self.assertEqual(updated.hourly_rate, Decimal("20.00"))
|
||||
self.assertEqual(updated.currency, "EUR")
|
||||
self.assertTrue(updated.is_active)
|
||||
|
||||
@pytest.fixture()
|
||||
def workspace(owner, admin, member):
|
||||
workspace = Workspace.objects.create(name="Rates", 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)
|
||||
return workspace
|
||||
def test_update_workspace_user_rate_updates_only_changed_fields(self):
|
||||
rate = WorkspaceUserRate.objects.create(
|
||||
workspace=self.workspace,
|
||||
user=self.member,
|
||||
hourly_rate=Decimal("10.00"),
|
||||
currency="USD",
|
||||
effective_from=self.workspace.created_at,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
updated = update_workspace_user_rate(
|
||||
rate,
|
||||
hourly_rate=Decimal("15.00"),
|
||||
currency="gbp",
|
||||
)
|
||||
|
||||
@pytest.fixture()
|
||||
def project(workspace, owner, admin, member):
|
||||
return Project.objects.create(workspace=workspace, name="Billing")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def price_units(db):
|
||||
PriceUnit.objects.create(code="USD", name="US Dollar", local_name="دلار آمریکا", symbol="$")
|
||||
PriceUnit.objects.create(code="EUR", name="Euro", local_name="یورو", symbol="€")
|
||||
|
||||
|
||||
def test_resolve_rate_uses_workspace_user_rate(workspace, project, member):
|
||||
WorkspaceUserRate.objects.create(
|
||||
workspace=workspace,
|
||||
user=member,
|
||||
hourly_rate=Decimal("40.00"),
|
||||
currency="EUR",
|
||||
effective_from=project.created_at,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
hourly_rate, currency = resolve_rate(member, project)
|
||||
|
||||
assert hourly_rate == Decimal("40.00")
|
||||
assert currency == "EUR"
|
||||
|
||||
|
||||
def test_resolve_rate_falls_back_to_workspace_user_rate(workspace, project, member):
|
||||
WorkspaceUserRate.objects.create(
|
||||
workspace=workspace,
|
||||
user=member,
|
||||
hourly_rate=Decimal("40.00"),
|
||||
currency="EUR",
|
||||
effective_from=project.created_at,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
hourly_rate, currency = resolve_rate(member, project)
|
||||
|
||||
assert hourly_rate == Decimal("40.00")
|
||||
assert currency == "EUR"
|
||||
|
||||
|
||||
def test_admin_can_manage_workspace_user_rates(api_client, admin, member, workspace, price_units):
|
||||
api_client.force_authenticate(user=admin)
|
||||
|
||||
create_response = api_client.post(
|
||||
"/api/workspace-user-rates/",
|
||||
{
|
||||
"workspace_id": str(workspace.id),
|
||||
"user_id": str(member.id),
|
||||
"hourly_rate": "35.50",
|
||||
"currency": "USD",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert create_response.status_code == 201
|
||||
rate_id = create_response.data["id"]
|
||||
assert WorkspaceUserRate.objects.filter(id=rate_id, is_deleted=False).exists()
|
||||
|
||||
update_response = api_client.patch(
|
||||
f"/api/workspace-user-rates/{rate_id}/",
|
||||
{"hourly_rate": "42.00"},
|
||||
format="json",
|
||||
)
|
||||
assert update_response.status_code == 200
|
||||
assert update_response.data["hourly_rate"] == "42.00"
|
||||
|
||||
delete_response = api_client.delete(f"/api/workspace-user-rates/{rate_id}/")
|
||||
assert delete_response.status_code == 204
|
||||
assert WorkspaceUserRate.all_objects.get(id=rate_id).is_deleted is True
|
||||
|
||||
|
||||
def test_member_cannot_manage_rates(api_client, member, workspace, price_units):
|
||||
api_client.force_authenticate(user=member)
|
||||
|
||||
workspace_response = api_client.post(
|
||||
"/api/workspace-user-rates/",
|
||||
{
|
||||
"workspace_id": str(workspace.id),
|
||||
"user_id": str(member.id),
|
||||
"hourly_rate": "25.00",
|
||||
"currency": "USD",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert workspace_response.status_code == 403
|
||||
self.assertEqual(updated.hourly_rate, Decimal("15.00"))
|
||||
self.assertEqual(updated.currency, "GBP")
|
||||
|
||||
Reference in New Issue
Block a user