229 lines
12 KiB
Python
229 lines
12 KiB
Python
|
|
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
|
||
|
|
|
||
|
|
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_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)
|