From 2790677407c5545efc7736f3557f3e184c66d9f5 Mon Sep 17 00:00:00 2001 From: Viswamedha Nalabotu Date: Sun, 18 Jan 2026 16:14:05 +0000 Subject: [PATCH] Moved role to organisation viewset and added extra tests --- apps/orgs/tests/test_api.py | 89 ++++++++++++++++++++++++++++++++++++- apps/orgs/viewsets.py | 53 +++++++++++++++++++++- 2 files changed, 139 insertions(+), 3 deletions(-) diff --git a/apps/orgs/tests/test_api.py b/apps/orgs/tests/test_api.py index d11faf3..ad7b82f 100644 --- a/apps/orgs/tests/test_api.py +++ b/apps/orgs/tests/test_api.py @@ -4,7 +4,7 @@ from django.utils import timezone from rest_framework.test import APIRequestFactory, force_authenticate from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND from apps.orgs.viewsets import OrganizationViewSet, InviteViewSet, RoleViewSet -from apps.orgs.models import Organization, OrganizationMembership, OrganizationInvitation +from apps.orgs.models import Organization, OrganizationMembership, OrganizationInvitation, Role User = get_user_model() @@ -98,6 +98,50 @@ class OrganizationAPITests(TestCase): resp = view(req, uuid=str(org.uuid)) self.assertEqual(resp.status_code, HTTP_403_FORBIDDEN) + def test_role_create_forbidden_for_non_manager(self): + org = Organization.objects.create(name='RoleNoCreateOrg', owner=self.user) + OrganizationMembership.objects.create(organization=org, user=self.user, is_manager=False) + view = OrganizationViewSet.as_view({'post': 'role'}) + req = self.factory.post('/', {'name': 'ForbiddenRole'}, format='json') + force_authenticate(req, user=self.user) + resp = view(req, uuid=str(org.uuid)) + self.assertEqual(resp.status_code, HTTP_403_FORBIDDEN) + + def test_role_members_post_missing_user_id_returns_400(self): + org = Organization.objects.create(name='RoleMissingParamOrg', owner=self.user) + OrganizationMembership.objects.create(organization=org, user=self.user, is_manager=True) + role = org.roles.create(name='Ops') + role_members_view = OrganizationViewSet.as_view({'post': 'role_members'}) + req = self.factory.post('/', {}, format='json') + force_authenticate(req, user=self.user) + resp = role_members_view(req, uuid=str(org.uuid), role_id=str(role.id)) + self.assertEqual(resp.status_code, HTTP_400_BAD_REQUEST) + + def test_role_members_post_non_manager_cannot_add_other_user(self): + org = Organization.objects.create(name='RoleAddForbiddenOrg', owner=self.user) + OrganizationMembership.objects.create(organization=org, user=self.user, is_manager=False) + target = User.objects.create_user(email_address='target@example.com', password='pass') + OrganizationMembership.objects.create(organization=org, user=target, is_manager=False) + role = org.roles.create(name='Contributor') + + role_members_view = OrganizationViewSet.as_view({'post': 'role_members'}) + req = self.factory.post('/', {'user_id': target.id}, format='json') + force_authenticate(req, user=self.user) + resp = role_members_view(req, uuid=str(org.uuid), role_id=str(role.id)) + self.assertEqual(resp.status_code, HTTP_403_FORBIDDEN) + + def test_role_members_get_outsider_returns_404(self): + org = Organization.objects.create(name='RoleOutsiderOrg', owner=self.user) + OrganizationMembership.objects.create(organization=org, user=self.user, is_manager=True) + role = org.roles.create(name='Viewer') + outsider = User.objects.create_user(email_address='outsider2@example.com', password='pass') + + role_members_view = OrganizationViewSet.as_view({'get': 'role_members', 'post': 'role_members'}) + req = self.factory.get('/') + force_authenticate(req, user=outsider) + resp = role_members_view(req, uuid=str(org.uuid), role_id=str(role.id)) + self.assertEqual(resp.status_code, HTTP_404_NOT_FOUND) + def test_non_member_cannot_view_org(self): other = User.objects.create_user(email_address='outside@example.com', password='pass') org = Organization.objects.create(name='HiddenOrg', owner=self.user) @@ -226,3 +270,46 @@ class OrganizationAPITests(TestCase): force_authenticate(req, user=self.user) resp = revoke_view(req, uuid=str(org.uuid), token=str(token.token)) self.assertEqual(resp.status_code, HTTP_403_FORBIDDEN) + + def test_role_create_and_visibility(self): + org = Organization.objects.create(name='RoleCreateOrg', owner=self.user) + OrganizationMembership.objects.create(organization=org, user=self.user, is_manager=True) + + org_role_view = OrganizationViewSet.as_view({'post': 'role'}) + req = self.factory.post('/', {'name': 'Tester'}, format='json') + force_authenticate(req, user=self.user) + resp = org_role_view(req, uuid=str(org.uuid)) + self.assertEqual(resp.status_code, HTTP_201_CREATED) + + view = RoleViewSet.as_view({'get': 'list'}) + req2 = self.factory.get('/') + force_authenticate(req2, user=self.user) + resp2 = view(req2) + self.assertEqual(resp2.status_code, HTTP_200_OK) + self.assertTrue(any(r['name'] == 'Tester' for r in resp2.data)) + + def test_role_members_get_and_post(self): + org = Organization.objects.create(name='RoleMembersOrg', owner=self.user) + OrganizationMembership.objects.create(organization=org, user=self.user, is_manager=True) + member = User.objects.create_user(email_address='memberrole@example.com', password='pass') + OrganizationMembership.objects.create(organization=org, user=member, is_manager=False) + role = org.roles.create(name='Developer') + + role_members_view = OrganizationViewSet.as_view({'get': 'role_members', 'post': 'role_members'}) + req = self.factory.get('/') + force_authenticate(req, user=self.user) + resp = role_members_view(req, uuid=str(org.uuid), role_id=str(role.id)) + self.assertEqual(resp.status_code, HTTP_200_OK) + + req2 = self.factory.post('/', {'user_id': member.id}, format='json') + force_authenticate(req2, user=self.user) + resp2 = role_members_view(req2, uuid=str(org.uuid), role_id=str(role.id)) + self.assertIn(resp2.status_code, (HTTP_200_OK, HTTP_201_CREATED)) + + mem = OrganizationMembership.objects.get(organization=org, user=member) + self.assertIsNotNone(mem) + + req3 = self.factory.post('/', {'user_id': member.id}, format='json') + force_authenticate(req3, user=member) + resp3 = role_members_view(req3, uuid=str(org.uuid), role_id=str(role.id)) + self.assertIn(resp3.status_code, (HTTP_200_OK, HTTP_201_CREATED)) diff --git a/apps/orgs/viewsets.py b/apps/orgs/viewsets.py index 807abee..13ed04a 100644 --- a/apps/orgs/viewsets.py +++ b/apps/orgs/viewsets.py @@ -6,8 +6,14 @@ from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_204_NO_CON from django.shortcuts import get_object_or_404 from django.utils import timezone from django.db.models import Q -from apps.orgs.models import Organization, OrganizationMembership, OrganizationInvitation, Role -from apps.orgs.serializers import OrganizationSerializer, OrganizationMembershipSerializer, OrganizationInvitationSerializer, RoleSerializer +from apps.orgs.models import Organization, OrganizationMembership, OrganizationInvitation, Role, RoleMembership +from apps.orgs.serializers import ( + OrganizationSerializer, + OrganizationMembershipSerializer, + OrganizationInvitationSerializer, + RoleSerializer, + RoleMembershipSerializer, +) class OrganizationViewSet(ModelViewSet): queryset = Organization.objects.all() @@ -98,6 +104,49 @@ class OrganizationViewSet(ModelViewSet): invite.save() return Response(status=HTTP_204_NO_CONTENT) + @action(detail=True, methods=['get', 'post'], url_path='role') + def role(self, request, uuid=None): + org = self.get_object() + + if request.method == 'GET': + roles = Role.objects.filter(organization=org) + serializer = RoleSerializer(roles, many=True) + return Response(serializer.data) + + membership = OrganizationMembership.objects.filter(organization=org, user=request.user, is_manager=True).first() + if not membership: + return Response({'error': 'Only managers can create roles'}, status=HTTP_403_FORBIDDEN) + + serializer = RoleSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(organization=org) + return Response(serializer.data, status=HTTP_201_CREATED) + return Response(serializer.errors, status=HTTP_400_BAD_REQUEST) + + @action(detail=True, methods=['get', 'post'], url_path='role/(?P[^/.]+)/members') + def role_members(self, request, uuid=None, role_id=None): + org = self.get_object() + role = get_object_or_404(Role, id=role_id, organization=org) + requester_membership = OrganizationMembership.objects.filter(organization=org, user=request.user).first() + if not requester_membership: + return Response(status=HTTP_404_NOT_FOUND) + + if request.method == 'GET': + memberships = RoleMembership.objects.filter(role=role) + serializer = RoleMembershipSerializer(memberships, many=True) + return Response(serializer.data) + manager_membership = OrganizationMembership.objects.filter(organization=org, user=request.user, is_manager=True).first() + user_id = request.data.get('user_id') + if not user_id: + return Response({'error': 'user_id is required'}, status=HTTP_400_BAD_REQUEST) + if request.user.id != int(user_id) and not manager_membership: + return Response({'error': 'Only managers can add other users to roles'}, status=HTTP_403_FORBIDDEN) + + role_membership, created = RoleMembership.objects.get_or_create(role=role, user_id=user_id) + + serializer = RoleMembershipSerializer(role_membership) + return Response(serializer.data, status=HTTP_201_CREATED if created else HTTP_200_OK) + class InviteViewSet(ModelViewSet): queryset = OrganizationInvitation.objects.all() serializer_class = OrganizationInvitationSerializer