from django.test import TestCase 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 rest_framework.test import APIClient from apps.accounts.models import Invite, Organization, Role, User class AccountsApiTests(TestCase): def setUp(self): self.client: APIClient = APIClient() self.manager: User = User.objects.create_user( email_address='manager@example.com', password='pass1234', first_name='Manager', last_name='User', date_of_birth='1990-01-01', is_manager=True, ) self.member: User = User.objects.create_user( email_address='member@example.com', password='pass1234', first_name='Member', last_name='User', date_of_birth='1992-02-02', ) self.other: User = User.objects.create_user( email_address='other@example.com', password='pass1234', first_name='Other', last_name='User', date_of_birth='1993-03-03', ) self.organization = Organization.objects.create( name='Team Alpha', description='Main team', owner=self.manager, ) self.organization.members.add(self.manager, self.member) self.role = Role.objects.create(name='Developer', organization=self.organization) def test_user_list_path(self): response = self.client.get('/api/user/') self.assertEqual(response.status_code, HTTP_200_OK) def test_user_retrieve_path(self): response = self.client.get(f'/api/user/{self.manager.uuid}/') self.assertEqual(response.status_code, HTTP_200_OK) def test_user_login_path(self): response = self.client.post('/api/user/login/', { 'email_address': 'manager@example.com', 'password': 'pass1234', }) self.assertEqual(response.status_code, HTTP_200_OK) self.assertTrue(response.json().get('success')) def test_user_logout_path(self): self.client.force_authenticate(self.manager) response = self.client.post('/api/user/logout/') self.assertEqual(response.status_code, HTTP_200_OK) def test_user_me_path(self): self.client.force_authenticate(self.member) response = self.client.get('/api/user/me/') self.assertEqual(response.status_code, HTTP_200_OK) self.assertEqual(response.json()['email_address'], 'member@example.com') def test_user_session_path(self): response = self.client.get('/api/user/session/') self.assertEqual(response.status_code, HTTP_200_OK) self.assertIn('isAuthenticated', response.json()) def test_user_signup_path(self): response = self.client.post('/api/user/signup/', { 'email_address': 'signup@example.com', 'password': 'newpass123', 'confirm_password': 'newpass123', 'first_name': 'Sign', 'last_name': 'Up', 'date_of_birth': '1995-05-05', 'manager': False, }, format='json') self.assertEqual(response.status_code, HTTP_201_CREATED) def test_user_change_password_path(self): self.client.force_authenticate(self.member) response = self.client.post('/api/user/change_password/', { 'old_password': 'pass1234', 'password': 'newpass123', 'confirm_password': 'newpass123', }, format='json') self.assertEqual(response.status_code, HTTP_200_OK) def test_organization_list_path(self): self.client.force_authenticate(self.manager) response = self.client.get('/api/organization/') self.assertEqual(response.status_code, HTTP_200_OK) def test_organization_create_path(self): self.client.force_authenticate(self.manager) response = self.client.post('/api/organization/', { 'name': 'Team Beta', 'description': 'Second team', }, format='json') self.assertEqual(response.status_code, HTTP_201_CREATED) def test_organization_retrieve_path(self): self.client.force_authenticate(self.member) response = self.client.get(f'/api/organization/{self.organization.uuid}/') self.assertEqual(response.status_code, HTTP_200_OK) def test_organization_update_path(self): self.client.force_authenticate(self.manager) response = self.client.put( f'/api/organization/{self.organization.uuid}/', {'name': 'Team Alpha Updated', 'description': 'Updated'}, format='json', ) self.assertEqual(response.status_code, HTTP_200_OK) def test_organization_partial_update_path(self): self.client.force_authenticate(self.manager) response = self.client.patch( f'/api/organization/{self.organization.uuid}/', {'description': 'Patched'}, format='json', ) self.assertEqual(response.status_code, HTTP_200_OK) def test_organization_delete_path(self): self.client.force_authenticate(self.manager) org = Organization.objects.create(name='Delete Me', owner=self.manager) org.members.add(self.manager) response = self.client.delete(f'/api/organization/{org.uuid}/') self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) def test_organization_invite_list_path(self): self.client.force_authenticate(self.manager) Invite.objects.create(organization=self.organization, created_by=self.manager) response = self.client.get(f'/api/invite/?organization_uuid={self.organization.uuid}') self.assertEqual(response.status_code, HTTP_200_OK) def test_organization_create_invite_path(self): self.client.force_authenticate(self.manager) response = self.client.post(f'/api/invite/?organization_uuid={self.organization.uuid}&max_uses=2', {}, format='json') self.assertEqual(response.status_code, HTTP_201_CREATED) self.assertIn('uuid', response.json()) def test_organization_revoke_invite_path(self): self.client.force_authenticate(self.manager) invite = Invite.objects.create(organization=self.organization, created_by=self.manager) response = self.client.delete(f'/api/invite/{invite.uuid}/?organization_uuid={self.organization.uuid}') self.assertEqual(response.status_code, HTTP_200_OK) def test_organization_join_path(self): self.client.force_authenticate(self.other) invite = Invite.objects.create(organization=self.organization, created_by=self.manager) response = self.client.post(f'/api/invite/join/?invite_uuid={invite.uuid}') self.assertEqual(response.status_code, HTTP_200_OK) def test_organization_leave_path(self): self.client.force_authenticate(self.member) self.role.members.add(self.member) response = self.client.post(f'/api/organization/{self.organization.uuid}/leave/') self.assertEqual(response.status_code, HTTP_200_OK) self.organization.refresh_from_db() self.assertFalse(self.organization.members.filter(uuid=self.member.uuid).exists()) self.assertFalse(self.role.members.filter(uuid=self.member.uuid).exists()) def test_organization_leave_owner_blocked_when_other_members_exist(self): self.client.force_authenticate(self.manager) response = self.client.post(f'/api/organization/{self.organization.uuid}/leave/') self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) self.assertIn('Owner cannot leave while other members/managers exist', response.json().get('error', '')) self.assertTrue(Organization.objects.filter(uuid=self.organization.uuid).exists()) def test_organization_leave_owner_deletes_org_when_no_other_members(self): solo_org = Organization.objects.create( name='Solo Owner Org', description='Only owner remains', owner=self.manager, ) solo_org.members.add(self.manager) self.client.force_authenticate(self.manager) response = self.client.post(f'/api/organization/{solo_org.uuid}/leave/') self.assertEqual(response.status_code, HTTP_200_OK) self.assertFalse(Organization.objects.filter(uuid=solo_org.uuid).exists()) def test_organization_members_path(self): self.client.force_authenticate(self.manager) response = self.client.get(f'/api/organization/{self.organization.uuid}/members/') self.assertEqual(response.status_code, HTTP_200_OK) def test_organization_remove_member_path(self): self.client.force_authenticate(self.manager) response = self.client.post(f'/api/organization/{self.organization.uuid}/member/{self.member.uuid}/remove/') self.assertEqual(response.status_code, HTTP_200_OK) def test_organization_roles_get_path(self): self.client.force_authenticate(self.manager) response = self.client.get(f'/api/role/?organization_uuid={self.organization.uuid}') self.assertEqual(response.status_code, HTTP_200_OK) def test_organization_roles_post_path(self): self.client.force_authenticate(self.manager) response = self.client.post( f'/api/role/?organization_uuid={self.organization.uuid}', {'name': 'Designer', 'description': 'Design role'}, format='json', ) self.assertEqual(response.status_code, HTTP_201_CREATED) def test_organization_my_roles_path(self): self.client.force_authenticate(self.member) self.role.members.add(self.member) response = self.client.get('/api/role/mine/') self.assertEqual(response.status_code, HTTP_200_OK) def test_organization_delete_role_path(self): self.client.force_authenticate(self.manager) delete_role = Role.objects.create(name='DeleteRole', organization=self.organization) response = self.client.delete(f'/api/role/{delete_role.uuid}/?organization_uuid={self.organization.uuid}') self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) def test_organization_join_role_path(self): self.client.force_authenticate(self.member) response = self.client.post(f'/api/role/{self.role.uuid}/join/?organization_uuid={self.organization.uuid}') self.assertEqual(response.status_code, HTTP_200_OK) def test_invite_create_rejects_non_integer_max_uses(self): self.client.force_authenticate(self.manager) response = self.client.post( f'/api/invite/?organization_uuid={self.organization.uuid}&max_uses=abc', {}, format='json', ) self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) self.assertIn('max_uses', response.json()) def test_invite_create_rejects_out_of_bounds_max_uses(self): self.client.force_authenticate(self.manager) low = self.client.post( f'/api/invite/?organization_uuid={self.organization.uuid}&max_uses=0', {}, format='json', ) high = self.client.post( f'/api/invite/?organization_uuid={self.organization.uuid}&max_uses=1001', {}, format='json', ) self.assertEqual(low.status_code, HTTP_400_BAD_REQUEST) self.assertEqual(high.status_code, HTTP_400_BAD_REQUEST) def test_invite_create_accepts_max_uses_boundaries(self): self.client.force_authenticate(self.manager) low = self.client.post( f'/api/invite/?organization_uuid={self.organization.uuid}&max_uses=1', {}, format='json', ) high = self.client.post( f'/api/invite/?organization_uuid={self.organization.uuid}&max_uses=1000', {}, format='json', ) self.assertEqual(low.status_code, HTTP_201_CREATED) self.assertEqual(high.status_code, HTTP_201_CREATED) def test_invite_join_fails_after_max_uses_reached(self): other_2 = User.objects.create_user( email_address='other2@example.com', password='pass1234', first_name='Other', last_name='Two', date_of_birth='1994-04-04', ) invite = Invite.objects.create( organization=self.organization, created_by=self.manager, max_uses=1, ) self.client.force_authenticate(self.other) first = self.client.post(f'/api/invite/join/?invite_uuid={invite.uuid}') self.assertEqual(first.status_code, HTTP_200_OK) self.client.force_authenticate(other_2) second = self.client.post(f'/api/invite/join/?invite_uuid={invite.uuid}') self.assertEqual(second.status_code, HTTP_400_BAD_REQUEST) self.assertIn('Invalid or expired invitation', second.json().get('error', '')) def test_non_manager_member_cannot_create_invite(self): self.client.force_authenticate(self.member) response = self.client.post( f'/api/invite/?organization_uuid={self.organization.uuid}&max_uses=2', {}, format='json', ) self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) def test_owner_cannot_be_removed_as_member(self): self.client.force_authenticate(self.manager) response = self.client.post( f'/api/organization/{self.organization.uuid}/member/{self.manager.uuid}/remove/' ) self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) def test_remove_member_not_found(self): self.client.force_authenticate(self.manager) response = self.client.post( f'/api/organization/{self.organization.uuid}/member/{self.other.uuid}/remove/' ) self.assertEqual(response.status_code, HTTP_404_NOT_FOUND) def test_role_name_can_repeat_across_organizations(self): second_org = Organization.objects.create( name='Team Beta Scoped Role', description='Second org', owner=self.manager, ) second_org.members.add(self.manager) self.client.force_authenticate(self.manager) response = self.client.post( f'/api/role/?organization_uuid={second_org.uuid}', {'name': 'Developer', 'description': 'Same role name in different org'}, format='json', ) self.assertEqual(response.status_code, HTTP_201_CREATED) def test_role_name_must_be_unique_within_organization(self): self.client.force_authenticate(self.manager) response = self.client.post( f'/api/role/?organization_uuid={self.organization.uuid}', {'name': 'Developer', 'description': 'Duplicate role in same org'}, format='json', ) self.assertEqual(response.status_code, HTTP_400_BAD_REQUEST) self.assertIn('name', response.json())