from django.contrib.auth import get_user_model from django.test import TestCase 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, Role User = get_user_model() class OrganizationAPITests(TestCase): def setUp(self): self.factory = APIRequestFactory() self.user = User.objects.create_user(email_address='apiuser@example.com', password='pass') def test_create_organization_creates_membership(self): data = {'name': 'API Org', 'description': 'Created via API'} view = OrganizationViewSet.as_view({'post': 'create'}) request = self.factory.post('/', data) force_authenticate(request, user=self.user) response = view(request) self.assertIn(response.status_code, (HTTP_201_CREATED, HTTP_200_OK)) org = Organization.objects.get(name='API Org') self.assertTrue(OrganizationMembership.objects.filter(organization=org, user=self.user).exists()) def test_invite_accept_flow(self): org = Organization.objects.create(name='InviteOrg', owner=self.user) OrganizationMembership.objects.create(organization=org, user=self.user, is_manager=True) org_view = OrganizationViewSet.as_view({'post': 'invites'}) request = self.factory.post('/', {}) force_authenticate(request, user=self.user) response = org_view(request, uuid=str(org.uuid)) self.assertEqual(response.status_code, HTTP_201_CREATED) token = response.data.get('token') other = User.objects.create_user(email_address='other@example.com', password='pass') invite_view = InviteViewSet.as_view({'post': 'accept'}) req2 = self.factory.post('/', {}) force_authenticate(req2, user=other) resp2 = invite_view(req2, token=token) self.assertIn(resp2.status_code, (HTTP_200_OK, HTTP_201_CREATED)) self.assertTrue(OrganizationMembership.objects.filter(organization=org, user=other).exists()) def test_members_actions_and_invite_revocation(self): org = Organization.objects.create(name='ActionsOrg', owner=self.user) OrganizationMembership.objects.create(organization=org, user=self.user, is_manager=True) member = User.objects.create_user(email_address='member@example.com', password='pass') OrganizationMembership.objects.create(organization=org, user=member, is_manager=False) members_view = OrganizationViewSet.as_view({'get': 'members'}) req = self.factory.get('/') force_authenticate(req, user=self.user) resp = members_view(req, uuid=str(org.uuid)) self.assertEqual(resp.status_code, HTTP_200_OK) self.assertTrue(any(m['user']['email_address'] == 'member@example.com' for m in resp.data)) update_view = OrganizationViewSet.as_view({'patch': 'update_member'}) req2 = self.factory.patch('/', {'is_manager': True}, format='json') force_authenticate(req2, user=self.user) resp2 = update_view(req2, uuid=str(org.uuid), user_id=str(member.id)) self.assertEqual(resp2.status_code, HTTP_200_OK) member.refresh_from_db() self.assertTrue(OrganizationMembership.objects.get(organization=org, user=member).is_manager) remove_view = OrganizationViewSet.as_view({'delete': 'remove_member'}) req3 = self.factory.delete('/') force_authenticate(req3, user=self.user) resp3 = remove_view(req3, uuid=str(org.uuid), user_id=str(org.owner.id)) self.assertEqual(resp3.status_code, HTTP_400_BAD_REQUEST) invites_view = OrganizationViewSet.as_view({'post': 'invites', 'get': 'invites'}) req4 = self.factory.post('/') force_authenticate(req4, user=self.user) resp4 = invites_view(req4, uuid=str(org.uuid)) self.assertEqual(resp4.status_code, HTTP_201_CREATED) token = resp4.data.get('token') req5 = self.factory.get('/') force_authenticate(req5, user=self.user) resp5 = invites_view(req5, uuid=str(org.uuid)) self.assertEqual(resp5.status_code, HTTP_200_OK) revoke_view = OrganizationViewSet.as_view({'delete': 'revoke_invite'}) req6 = self.factory.delete('/') force_authenticate(req6, user=self.user) resp6 = revoke_view(req6, uuid=str(org.uuid), token=str(token)) self.assertEqual(resp6.status_code, HTTP_204_NO_CONTENT) self.assertFalse(OrganizationInvitation.objects.filter(token=token, is_active=True).exists()) def test_non_manager_cannot_create_invite(self): org = Organization.objects.create(name='NoCreateOrg', owner=self.user) OrganizationMembership.objects.create(organization=org, user=self.user, is_manager=False) view = OrganizationViewSet.as_view({'post': 'invites'}) req = self.factory.post('/') force_authenticate(req, user=self.user) 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) view = OrganizationViewSet.as_view({'get': 'retrieve'}) req = self.factory.get('/') force_authenticate(req, user=other) resp = view(req, uuid=str(org.uuid)) self.assertEqual(resp.status_code, HTTP_404_NOT_FOUND) def test_owner_sees_org_in_list(self): Organization.objects.create(name='OwnerListOrg', owner=self.user) view = OrganizationViewSet.as_view({'get': 'list'}) req = self.factory.get('/') force_authenticate(req, user=self.user) resp = view(req) self.assertEqual(resp.status_code, HTTP_200_OK) self.assertTrue(any(o['name'] == 'OwnerListOrg' for o in resp.data)) def test_member_sees_org_in_list(self): other = User.objects.create_user(email_address='member2@example.com', password='pass') org = Organization.objects.create(name='MemberListOrg', owner=self.user) OrganizationMembership.objects.create(organization=org, user=other, is_manager=False) view = OrganizationViewSet.as_view({'get': 'list'}) req = self.factory.get('/') force_authenticate(req, user=other) resp = view(req) self.assertEqual(resp.status_code, HTTP_200_OK) self.assertTrue(any(o['name'] == 'MemberListOrg' for o in resp.data)) def test_non_member_not_in_list(self): outsider = User.objects.create_user(email_address='outsider@example.com', password='pass') Organization.objects.create(name='HiddenOrg2', owner=self.user) view = OrganizationViewSet.as_view({'get': 'list'}) req = self.factory.get('/') force_authenticate(req, user=outsider) resp = view(req) self.assertEqual(resp.status_code, HTTP_200_OK) self.assertFalse(any(o['name'] == 'HiddenOrg2' for o in resp.data)) def test_roles_visible_to_owner_and_member_but_not_outsider(self): owner = self.user member = User.objects.create_user(email_address='rmember@example.com', password='pass') outsider = User.objects.create_user(email_address='routsider@example.com', password='pass') org = Organization.objects.create(name='RoleOrg2', owner=owner) OrganizationMembership.objects.create(organization=org, user=member, is_manager=False) role = org.roles.create(name='Tester') view = RoleViewSet.as_view({'get': 'list'}) req = self.factory.get('/') force_authenticate(req, user=owner) resp = view(req) self.assertEqual(resp.status_code, HTTP_200_OK) self.assertTrue(any(r['name'] == 'Tester' for r in resp.data)) req2 = self.factory.get('/') force_authenticate(req2, user=member) resp2 = view(req2) self.assertEqual(resp2.status_code, HTTP_200_OK) self.assertTrue(any(r['name'] == 'Tester' for r in resp2.data)) req3 = self.factory.get('/') force_authenticate(req3, user=outsider) resp3 = view(req3) self.assertEqual(resp3.status_code, HTTP_200_OK) self.assertFalse(any(r['name'] == 'Tester' for r in resp3.data)) def test_members_endpoint_only_accessible_to_members(self): org = Organization.objects.create(name='MemberOnlyOrg', owner=self.user) member = User.objects.create_user(email_address='monly@example.com', password='pass') OrganizationMembership.objects.create(organization=org, user=member, is_manager=False) members_view = OrganizationViewSet.as_view({'get': 'members'}) req = self.factory.get('/') force_authenticate(req, user=member) resp = members_view(req, uuid=str(org.uuid)) self.assertEqual(resp.status_code, HTTP_200_OK) outsider = User.objects.create_user(email_address='notmem@example.com', password='pass') req2 = self.factory.get('/') force_authenticate(req2, user=outsider) resp2 = members_view(req2, uuid=str(org.uuid)) self.assertEqual(resp2.status_code, HTTP_404_NOT_FOUND) def test_invite_accept_invalid_or_expired(self): org = Organization.objects.create(name='InvalidInviteOrg', owner=self.user) OrganizationMembership.objects.create(organization=org, user=self.user, is_manager=True) invite = OrganizationInvitation.objects.create(organization=org, created_by=self.user) invite.expires_at = invite.created_at - timezone.timedelta(days=1) invite.save() other = User.objects.create_user(email_address='inviter2@example.com', password='pass') invite_view = InviteViewSet.as_view({'post': 'accept'}) req = self.factory.post('/') force_authenticate(req, user=other) resp = invite_view(req, token=str(invite.token)) self.assertIn(resp.status_code, (HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND)) def test_remove_member_by_non_manager_forbidden(self): org = Organization.objects.create(name='RemoveForbidOrg', owner=self.user) OrganizationMembership.objects.create(organization=org, user=self.user, is_manager=False) member = User.objects.create_user(email_address='m2@example.com', password='pass') OrganizationMembership.objects.create(organization=org, user=member, is_manager=False) remove_view = OrganizationViewSet.as_view({'delete': 'remove_member'}) req = self.factory.delete('/') force_authenticate(req, user=self.user) resp = remove_view(req, uuid=str(org.uuid), user_id=str(member.id)) self.assertEqual(resp.status_code, HTTP_403_FORBIDDEN) def test_update_member_by_non_manager_forbidden(self): org = Organization.objects.create(name='UpdateForbidOrg', owner=self.user) OrganizationMembership.objects.create(organization=org, user=self.user, is_manager=False) member = User.objects.create_user(email_address='m3@example.com', password='pass') OrganizationMembership.objects.create(organization=org, user=member, is_manager=False) update_view = OrganizationViewSet.as_view({'patch': 'update_member'}) req = self.factory.patch('/', {'is_manager': True}, format='json') force_authenticate(req, user=self.user) resp = update_view(req, uuid=str(org.uuid), user_id=str(member.id)) self.assertEqual(resp.status_code, HTTP_403_FORBIDDEN) def test_invite_revoke_by_non_manager_forbidden(self): org = Organization.objects.create(name='RevokeForbidOrg', owner=self.user) OrganizationMembership.objects.create(organization=org, user=self.user, is_manager=False) OrganizationMembership.objects.create(organization=org, user=User.objects.create_user(email_address='mgr@example.com', password='p'), is_manager=True) token = OrganizationInvitation.objects.create(organization=org, created_by=self.user) revoke_view = OrganizationViewSet.as_view({'delete': 'revoke_invite'}) req = self.factory.delete('/') 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))