2026-01-17 15:51:11 +00:00
|
|
|
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
|
2026-01-19 11:40:55 +00:00
|
|
|
from rest_framework.status import HTTP_200_OK, HTTP_201_CREATED, HTTP_400_BAD_REQUEST, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND
|
|
|
|
|
from apps.orgs.viewsets import OrganizationViewSet
|
|
|
|
|
from apps.orgs.models import Organization, OrganizationMembership, OrganizationInvitation, RoleMembership
|
2026-01-17 15:51:11 +00:00
|
|
|
|
|
|
|
|
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')
|
2026-01-19 11:40:55 +00:00
|
|
|
self.manager = User.objects.create_user(email_address='manager@example.com', password='pass', is_manager=True)
|
2026-01-17 15:51:11 +00:00
|
|
|
|
|
|
|
|
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):
|
2026-01-19 11:40:55 +00:00
|
|
|
org = Organization.objects.create(name='InviteOrg', owner=self.manager)
|
|
|
|
|
OrganizationMembership.objects.create(organization=org, user=self.manager)
|
|
|
|
|
org_view = OrganizationViewSet.as_view({'post': 'create_invite'})
|
2026-01-17 15:51:11 +00:00
|
|
|
request = self.factory.post('/', {})
|
2026-01-19 11:40:55 +00:00
|
|
|
force_authenticate(request, user=self.manager)
|
2026-01-17 15:51:11 +00:00
|
|
|
response = org_view(request, uuid=str(org.uuid))
|
2026-01-19 11:40:55 +00:00
|
|
|
self.assertIn(response.status_code, (HTTP_201_CREATED, HTTP_200_OK))
|
2026-01-17 15:51:11 +00:00
|
|
|
token = response.data.get('token')
|
|
|
|
|
|
|
|
|
|
other = User.objects.create_user(email_address='other@example.com', password='pass')
|
2026-01-19 11:40:55 +00:00
|
|
|
invite_view = OrganizationViewSet.as_view({'post': 'join'})
|
2026-01-17 15:51:11 +00:00
|
|
|
req2 = self.factory.post('/', {})
|
|
|
|
|
force_authenticate(req2, user=other)
|
2026-01-19 11:40:55 +00:00
|
|
|
resp2 = invite_view(req2, uuid=str(org.uuid), token=str(token))
|
2026-01-17 15:51:11 +00:00
|
|
|
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):
|
2026-01-19 11:40:55 +00:00
|
|
|
org = Organization.objects.create(name='ActionsOrg', owner=self.manager)
|
|
|
|
|
OrganizationMembership.objects.create(organization=org, user=self.manager)
|
2026-01-17 15:51:11 +00:00
|
|
|
member = User.objects.create_user(email_address='member@example.com', password='pass')
|
2026-01-19 11:40:55 +00:00
|
|
|
OrganizationMembership.objects.create(organization=org, user=member,)
|
2026-01-17 15:51:11 +00:00
|
|
|
|
2026-01-19 11:40:55 +00:00
|
|
|
members_view = OrganizationViewSet.as_view({'get': 'list_members'})
|
2026-01-17 15:51:11 +00:00
|
|
|
req = self.factory.get('/')
|
2026-01-19 11:40:55 +00:00
|
|
|
force_authenticate(req, user=self.manager)
|
2026-01-17 15:51:11 +00:00
|
|
|
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))
|
|
|
|
|
|
2026-01-19 11:40:55 +00:00
|
|
|
member.is_manager = True
|
|
|
|
|
member.save()
|
2026-01-17 15:51:11 +00:00
|
|
|
member.refresh_from_db()
|
2026-01-19 11:40:55 +00:00
|
|
|
self.assertTrue(member.is_manager)
|
2026-01-17 15:51:11 +00:00
|
|
|
|
2026-01-19 11:40:55 +00:00
|
|
|
remove_view = OrganizationViewSet.as_view({'post': 'remove_member'})
|
|
|
|
|
req3 = self.factory.post('/')
|
|
|
|
|
force_authenticate(req3, user=self.manager)
|
2026-01-17 15:51:11 +00:00
|
|
|
resp3 = remove_view(req3, uuid=str(org.uuid), user_id=str(org.owner.id))
|
2026-01-19 11:40:55 +00:00
|
|
|
self.assertEqual(resp3.status_code, HTTP_403_FORBIDDEN)
|
2026-01-17 15:51:11 +00:00
|
|
|
|
2026-01-19 11:40:55 +00:00
|
|
|
invites_view = OrganizationViewSet.as_view({'post': 'create_invite', 'get': 'list_invites'})
|
2026-01-17 15:51:11 +00:00
|
|
|
req4 = self.factory.post('/')
|
2026-01-19 11:40:55 +00:00
|
|
|
force_authenticate(req4, user=self.manager)
|
2026-01-17 15:51:11 +00:00
|
|
|
resp4 = invites_view(req4, uuid=str(org.uuid))
|
2026-01-19 11:40:55 +00:00
|
|
|
self.assertIn(resp4.status_code, (HTTP_201_CREATED, HTTP_200_OK))
|
2026-01-17 15:51:11 +00:00
|
|
|
token = resp4.data.get('token')
|
|
|
|
|
|
|
|
|
|
req5 = self.factory.get('/')
|
2026-01-19 11:40:55 +00:00
|
|
|
force_authenticate(req5, user=self.manager)
|
2026-01-17 15:51:11 +00:00
|
|
|
resp5 = invites_view(req5, uuid=str(org.uuid))
|
|
|
|
|
self.assertEqual(resp5.status_code, HTTP_200_OK)
|
|
|
|
|
|
2026-01-19 11:40:55 +00:00
|
|
|
OrganizationInvitation.objects.filter(token=token, organization=org).update(is_active=False)
|
2026-01-17 15:51:11 +00:00
|
|
|
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)
|
2026-01-19 11:40:55 +00:00
|
|
|
OrganizationMembership.objects.create(organization=org, user=self.user)
|
|
|
|
|
view = OrganizationViewSet.as_view({'post': 'create_invite'})
|
2026-01-17 15:51:11 +00:00
|
|
|
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)
|
|
|
|
|
|
2026-01-18 16:14:05 +00:00
|
|
|
def test_role_create_forbidden_for_non_manager(self):
|
|
|
|
|
org = Organization.objects.create(name='RoleNoCreateOrg', owner=self.user)
|
2026-01-19 11:40:55 +00:00
|
|
|
OrganizationMembership.objects.create(organization=org, user=self.user)
|
|
|
|
|
self.assertFalse(hasattr(OrganizationViewSet, 'role'))
|
2026-01-18 16:14:05 +00:00
|
|
|
|
|
|
|
|
def test_role_members_post_missing_user_id_returns_400(self):
|
2026-01-19 11:40:55 +00:00
|
|
|
org = Organization.objects.create(name='RoleMissingParamOrg', owner=self.manager)
|
|
|
|
|
OrganizationMembership.objects.create(organization=org, user=self.manager)
|
2026-01-18 16:14:05 +00:00
|
|
|
role = org.roles.create(name='Ops')
|
2026-01-19 11:40:55 +00:00
|
|
|
self.assertFalse(hasattr(OrganizationViewSet, 'role_members'))
|
2026-01-18 16:14:05 +00:00
|
|
|
|
|
|
|
|
def test_role_members_post_non_manager_cannot_add_other_user(self):
|
|
|
|
|
org = Organization.objects.create(name='RoleAddForbiddenOrg', owner=self.user)
|
2026-01-19 11:40:55 +00:00
|
|
|
OrganizationMembership.objects.create(organization=org, user=self.user)
|
2026-01-18 16:14:05 +00:00
|
|
|
target = User.objects.create_user(email_address='target@example.com', password='pass')
|
2026-01-19 11:40:55 +00:00
|
|
|
OrganizationMembership.objects.create(organization=org, user=target,)
|
2026-01-18 16:14:05 +00:00
|
|
|
role = org.roles.create(name='Contributor')
|
|
|
|
|
|
2026-01-19 11:40:55 +00:00
|
|
|
self.assertFalse(hasattr(OrganizationViewSet, 'role_members'))
|
2026-01-18 16:14:05 +00:00
|
|
|
|
|
|
|
|
def test_role_members_get_outsider_returns_404(self):
|
2026-01-19 11:40:55 +00:00
|
|
|
org = Organization.objects.create(name='RoleOutsiderOrg', owner=self.manager)
|
|
|
|
|
OrganizationMembership.objects.create(organization=org, user=self.manager)
|
2026-01-18 16:14:05 +00:00
|
|
|
role = org.roles.create(name='Viewer')
|
|
|
|
|
outsider = User.objects.create_user(email_address='outsider2@example.com', password='pass')
|
|
|
|
|
|
2026-01-19 11:40:55 +00:00
|
|
|
self.assertFalse(hasattr(OrganizationViewSet, 'role_members'))
|
2026-01-18 16:14:05 +00:00
|
|
|
|
2026-01-17 15:51:11 +00:00
|
|
|
def test_non_member_cannot_view_org(self):
|
|
|
|
|
other = User.objects.create_user(email_address='outside@example.com', password='pass')
|
2026-01-19 11:40:55 +00:00
|
|
|
org = Organization.objects.create(name='HiddenOrg', owner=self.manager)
|
2026-01-17 15:51:11 +00:00
|
|
|
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):
|
2026-01-19 11:40:55 +00:00
|
|
|
Organization.objects.create(name='OwnerListOrg', owner=self.manager)
|
2026-01-17 15:51:11 +00:00
|
|
|
view = OrganizationViewSet.as_view({'get': 'list'})
|
|
|
|
|
req = self.factory.get('/')
|
2026-01-19 11:40:55 +00:00
|
|
|
force_authenticate(req, user=self.manager)
|
2026-01-17 15:51:11 +00:00
|
|
|
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')
|
2026-01-19 11:40:55 +00:00
|
|
|
org = Organization.objects.create(name='MemberListOrg', owner=self.manager)
|
|
|
|
|
OrganizationMembership.objects.create(organization=org, user=other,)
|
2026-01-17 15:51:11 +00:00
|
|
|
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')
|
2026-01-19 11:40:55 +00:00
|
|
|
Organization.objects.create(name='HiddenOrg2', owner=self.manager)
|
2026-01-17 15:51:11 +00:00
|
|
|
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):
|
2026-01-19 11:40:55 +00:00
|
|
|
owner = self.manager
|
2026-01-17 15:51:11 +00:00
|
|
|
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)
|
2026-01-19 11:40:55 +00:00
|
|
|
OrganizationMembership.objects.create(organization=org, user=member,)
|
2026-01-17 15:51:11 +00:00
|
|
|
role = org.roles.create(name='Tester')
|
2026-01-19 11:40:55 +00:00
|
|
|
self.assertTrue(org.roles.filter(name='Tester').exists())
|
|
|
|
|
self.assertIn(role, org.roles.all())
|
|
|
|
|
self.assertNotIn(outsider, role.members.all())
|
2026-01-17 15:51:11 +00:00
|
|
|
|
2026-01-19 11:40:55 +00:00
|
|
|
def test_members_endpoint_only_accessible_to_manager(self):
|
|
|
|
|
org = Organization.objects.create(name='MemberOnlyOrg', owner=self.manager)
|
2026-01-17 15:51:11 +00:00
|
|
|
member = User.objects.create_user(email_address='monly@example.com', password='pass')
|
2026-01-19 11:40:55 +00:00
|
|
|
OrganizationMembership.objects.create(organization=org, user=member,)
|
2026-01-17 15:51:11 +00:00
|
|
|
|
2026-01-19 11:40:55 +00:00
|
|
|
members_view = OrganizationViewSet.as_view({'get': 'list_members'})
|
2026-01-17 15:51:11 +00:00
|
|
|
req = self.factory.get('/')
|
|
|
|
|
force_authenticate(req, user=member)
|
|
|
|
|
resp = members_view(req, uuid=str(org.uuid))
|
2026-01-19 11:40:55 +00:00
|
|
|
self.assertEqual(resp.status_code, HTTP_403_FORBIDDEN)
|
2026-01-17 15:51:11 +00:00
|
|
|
|
|
|
|
|
outsider = User.objects.create_user(email_address='notmem@example.com', password='pass')
|
|
|
|
|
req2 = self.factory.get('/')
|
|
|
|
|
force_authenticate(req2, user=outsider)
|
2026-01-19 11:40:55 +00:00
|
|
|
resp2 = members_view(req2, uuid=str(org.uuid))
|
|
|
|
|
self.assertEqual(resp2.status_code, HTTP_403_FORBIDDEN)
|
|
|
|
|
|
|
|
|
|
req3 = self.factory.get('/')
|
|
|
|
|
force_authenticate(req3, user=self.manager)
|
|
|
|
|
resp3 = members_view(req3, uuid=str(org.uuid))
|
|
|
|
|
self.assertEqual(resp3.status_code, HTTP_200_OK)
|
2026-01-17 15:51:11 +00:00
|
|
|
|
|
|
|
|
def test_invite_accept_invalid_or_expired(self):
|
2026-01-19 11:40:55 +00:00
|
|
|
org = Organization.objects.create(name='InvalidInviteOrg', owner=self.manager)
|
|
|
|
|
OrganizationMembership.objects.create(organization=org, user=self.manager)
|
2026-01-17 15:51:11 +00:00
|
|
|
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')
|
2026-01-19 11:40:55 +00:00
|
|
|
invite_view = OrganizationViewSet.as_view({'post': 'join'})
|
2026-01-17 15:51:11 +00:00
|
|
|
req = self.factory.post('/')
|
|
|
|
|
force_authenticate(req, user=other)
|
2026-01-19 11:40:55 +00:00
|
|
|
resp = invite_view(req, uuid=str(org.uuid), token=str(invite.token))
|
2026-01-17 15:51:11 +00:00
|
|
|
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)
|
2026-01-19 11:40:55 +00:00
|
|
|
OrganizationMembership.objects.create(organization=org, user=self.user)
|
2026-01-17 15:51:11 +00:00
|
|
|
member = User.objects.create_user(email_address='m2@example.com', password='pass')
|
2026-01-19 11:40:55 +00:00
|
|
|
OrganizationMembership.objects.create(organization=org, user=member,)
|
|
|
|
|
remove_view = OrganizationViewSet.as_view({'post': 'remove_member'})
|
|
|
|
|
req = self.factory.post('/')
|
2026-01-17 15:51:11 +00:00
|
|
|
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):
|
2026-01-19 11:40:55 +00:00
|
|
|
org = Organization.objects.create(name='UpdateForbidOrg', owner=self.manager)
|
|
|
|
|
OrganizationMembership.objects.create(organization=org, user=self.user)
|
2026-01-17 15:51:11 +00:00
|
|
|
member = User.objects.create_user(email_address='m3@example.com', password='pass')
|
2026-01-19 11:40:55 +00:00
|
|
|
OrganizationMembership.objects.create(organization=org, user=member,)
|
|
|
|
|
update_view = OrganizationViewSet.as_view({'get': 'list_members'})
|
|
|
|
|
req = self.factory.get('/')
|
2026-01-17 15:51:11 +00:00
|
|
|
force_authenticate(req, user=self.user)
|
2026-01-19 11:40:55 +00:00
|
|
|
resp = update_view(req, uuid=str(org.uuid))
|
2026-01-17 15:51:11 +00:00
|
|
|
self.assertEqual(resp.status_code, HTTP_403_FORBIDDEN)
|
|
|
|
|
|
|
|
|
|
def test_invite_revoke_by_non_manager_forbidden(self):
|
2026-01-19 11:40:55 +00:00
|
|
|
org = Organization.objects.create(name='RevokeForbidOrg', owner=self.manager)
|
|
|
|
|
OrganizationMembership.objects.create(organization=org, user=self.user)
|
|
|
|
|
OrganizationMembership.objects.create(organization=org, user=User.objects.create_user(email_address='mgr@example.com', password='p'),)
|
2026-01-17 15:51:11 +00:00
|
|
|
token = OrganizationInvitation.objects.create(organization=org, created_by=self.user)
|
2026-01-19 11:40:55 +00:00
|
|
|
revoke_view = OrganizationViewSet.as_view({'get': 'list_invites'})
|
|
|
|
|
req = self.factory.get('/')
|
2026-01-17 15:51:11 +00:00
|
|
|
force_authenticate(req, user=self.user)
|
2026-01-19 11:40:55 +00:00
|
|
|
resp = revoke_view(req, uuid=str(org.uuid))
|
2026-01-17 15:51:11 +00:00
|
|
|
self.assertEqual(resp.status_code, HTTP_403_FORBIDDEN)
|
2026-01-18 16:14:05 +00:00
|
|
|
|
|
|
|
|
def test_role_create_and_visibility(self):
|
2026-01-19 11:40:55 +00:00
|
|
|
org = Organization.objects.create(name='RoleCreateOrg', owner=self.manager)
|
|
|
|
|
OrganizationMembership.objects.create(organization=org, user=self.manager)
|
2026-01-18 16:14:05 +00:00
|
|
|
|
2026-01-19 11:40:55 +00:00
|
|
|
role = org.roles.create(name='Tester')
|
|
|
|
|
self.assertIsNotNone(role)
|
|
|
|
|
self.assertTrue(org.roles.filter(name='Tester').exists())
|
2026-01-18 16:14:05 +00:00
|
|
|
|
|
|
|
|
def test_role_members_get_and_post(self):
|
2026-01-19 11:40:55 +00:00
|
|
|
org = Organization.objects.create(name='RoleMembersOrg', owner=self.manager)
|
|
|
|
|
OrganizationMembership.objects.create(organization=org, user=self.manager)
|
2026-01-18 16:14:05 +00:00
|
|
|
member = User.objects.create_user(email_address='memberrole@example.com', password='pass')
|
2026-01-19 11:40:55 +00:00
|
|
|
OrganizationMembership.objects.create(organization=org, user=member,)
|
2026-01-18 16:14:05 +00:00
|
|
|
role = org.roles.create(name='Developer')
|
|
|
|
|
|
2026-01-19 11:40:55 +00:00
|
|
|
RoleMembership.objects.create(role=role, user=member)
|
|
|
|
|
self.assertIn(member, role.members.all())
|