from decimal import Decimal from rest_framework.test import APITestCase from apps.clients.models import Client from apps.projects.models import Project, ProjectAccess, ProjectUserRate from apps.users.models import User from apps.workspaces.models import PriceUnit, Workspace, WorkspaceMembership, WorkspaceUserRate class ProjectViewTests(APITestCase): @classmethod def setUpTestData(cls): cls.owner = User.objects.create_user( mobile="09121110001", password="secret123", first_name="Owner", ) cls.workspace = Workspace.objects.create(name="Projects", owner=cls.owner) PriceUnit.objects.create(code="USD", name="US Dollar", local_name="Dollar", symbol="$") cls.member = User.objects.create_user( mobile="09121110002", password="secret123", first_name="Member", ) WorkspaceMembership.objects.create( workspace=cls.workspace, user=cls.member, role=WorkspaceMembership.Role.MEMBER, is_active=True, ) cls.first_client = Client.objects.create(workspace=cls.workspace, name="Acme") cls.second_client = Client.objects.create(workspace=cls.workspace, name="Globex") cls.third_client = Client.objects.create(workspace=cls.workspace, name="Initech") Project.objects.create( workspace=cls.workspace, client=cls.first_client, name="Alpha", ) cls.second_project = Project.objects.create( workspace=cls.workspace, client=cls.second_client, name="Beta", ) cls.third_project = Project.objects.create( workspace=cls.workspace, client=cls.third_client, name="Gamma", ) cls.first_project = Project.objects.get(name="Alpha") ProjectAccess.objects.create(project=cls.first_project, user=cls.member) ProjectAccess.objects.create(project=cls.second_project, user=cls.member) WorkspaceUserRate.objects.create( workspace=cls.workspace, user=cls.member, hourly_rate=Decimal("25.00"), currency="USD", effective_from=cls.workspace.created_at, is_active=True, ) def test_project_list_supports_multi_client_filter(self): self.client.force_authenticate(user=self.member) response = self.client.get( "/api/projects/", [ ("workspace", str(self.workspace.id)), ("clients", str(self.first_client.id)), ("clients", str(self.second_client.id)), ], ) self.assertEqual(response.status_code, 200) items = ( response.data if isinstance(response.data, list) else response.data.get("results") or response.data.get("items", []) ) result_ids = {str(item["client"]["id"]) for item in items} self.assertEqual( result_ids, {str(self.first_client.id), str(self.second_client.id)}, ) def test_project_access_list_and_mutations_require_explicit_member_access(self): self.client.force_authenticate(user=self.owner) access_response = self.client.get( "/api/projects/access/", {"workspace": str(self.workspace.id), "user": str(self.member.id)}, ) self.assertEqual(access_response.status_code, 200) items = access_response.data["items"] gamma_item = next(item for item in items if item["id"] == str(self.third_project.id)) self.assertFalse(gamma_item["has_access"]) alpha_item = next(item for item in items if item["id"] == str(self.first_project.id)) self.assertEqual(alpha_item["workspace_rate"]["hourly_rate"], "25.00") self.assertIsNone(alpha_item["project_rate"]) grant_response = self.client.post( "/api/projects/access/grant/", { "workspace": str(self.workspace.id), "user": str(self.member.id), "project_ids": [str(self.third_project.id)], }, format="json", ) self.assertEqual(grant_response.status_code, 200) access_response = self.client.get( "/api/projects/access/", {"workspace": str(self.workspace.id), "user": str(self.member.id)}, ) gamma_item = next(item for item in access_response.data["items"] if item["id"] == str(self.third_project.id)) self.assertTrue(gamma_item["has_access"]) revoke_response = self.client.post( "/api/projects/access/revoke/", { "workspace": str(self.workspace.id), "user": str(self.member.id), "project_ids": [str(self.first_project.id)], }, format="json", ) self.assertEqual(revoke_response.status_code, 200) self.assertFalse(ProjectAccess.objects.filter(project=self.first_project, user=self.member).exists()) def test_project_access_rate_endpoint_saves_override_and_keeps_it_dormant_after_revoke(self): self.client.force_authenticate(user=self.owner) save_response = self.client.post( "/api/projects/access/rate/", { "workspace": str(self.workspace.id), "user": str(self.member.id), "project": str(self.first_project.id), "hourly_rate": "44.50", "currency": "USD", }, format="json", ) self.assertEqual(save_response.status_code, 200) self.assertFalse(save_response.data["removed"]) self.assertEqual(save_response.data["item"]["project_rate"]["hourly_rate"], "44.50") self.assertTrue( ProjectUserRate.objects.filter(project=self.first_project, user=self.member, is_deleted=False).exists() ) revoke_response = self.client.post( "/api/projects/access/revoke/", { "workspace": str(self.workspace.id), "user": str(self.member.id), "project_ids": [str(self.first_project.id)], }, format="json", ) self.assertEqual(revoke_response.status_code, 200) self.assertTrue( ProjectUserRate.objects.filter(project=self.first_project, user=self.member, is_deleted=False).exists() ) access_response = self.client.get( "/api/projects/access/", {"workspace": str(self.workspace.id), "user": str(self.member.id)}, ) self.assertEqual(access_response.status_code, 200) alpha_item = next(item for item in access_response.data["items"] if item["id"] == str(self.first_project.id)) self.assertFalse(alpha_item["has_access"]) self.assertEqual(alpha_item["project_rate"]["hourly_rate"], "44.50") def test_project_access_rate_endpoint_rejects_projects_without_access(self): self.client.force_authenticate(user=self.owner) response = self.client.post( "/api/projects/access/rate/", { "workspace": str(self.workspace.id), "user": str(self.member.id), "project": str(self.third_project.id), "hourly_rate": "44.50", "currency": "USD", }, format="json", ) self.assertEqual(response.status_code, 400) self.assertIn("Grant project access", response.data["detail"])