|
11 | 11 | from ddt import data, ddt, unpack |
12 | 12 | from django.contrib.auth import get_user_model |
13 | 13 | from django.urls import reverse |
| 14 | +from organizations.models import Organization |
14 | 15 | from rest_framework import status |
15 | 16 | from rest_framework.test import APIClient |
16 | 17 |
|
@@ -853,6 +854,200 @@ def test_put_accepts_valid_full_course_key_scope(self, _mock_exists, _mock_assig |
853 | 854 | self.assertEqual(len(response.data["completed"]), 1) |
854 | 855 |
|
855 | 856 |
|
| 857 | +@ddt |
| 858 | +class TestAdminConsoleOrgsAPIView(ViewTestMixin): |
| 859 | + """Test suite for AdminConsoleOrgsAPIView.""" |
| 860 | + |
| 861 | + @classmethod |
| 862 | + def setUpClass(cls): |
| 863 | + """Assign a course role to regular_9 for COURSES_VIEW_COURSE_TEAM permission tests.""" |
| 864 | + super().setUpClass() |
| 865 | + cls._assign_roles_to_users( |
| 866 | + [ |
| 867 | + { |
| 868 | + "subject_name": "regular_9", |
| 869 | + "role_name": roles.COURSE_STAFF.external_key, |
| 870 | + "scope_name": "course-v1:Org1+COURSE1+2024", |
| 871 | + }, |
| 872 | + ] |
| 873 | + ) |
| 874 | + |
| 875 | + @classmethod |
| 876 | + def setUpTestData(cls): |
| 877 | + """Create Organization fixtures.""" |
| 878 | + super().setUpTestData() |
| 879 | + |
| 880 | + Organization.objects.bulk_create( |
| 881 | + [ |
| 882 | + Organization(name="Alpha University", short_name="AlphaU"), |
| 883 | + Organization(name="Beta Institute", short_name="BetaI"), |
| 884 | + Organization(name="Gamma College", short_name="GammaC"), |
| 885 | + ] |
| 886 | + ) |
| 887 | + |
| 888 | + def setUp(self): |
| 889 | + """Set up test fixtures.""" |
| 890 | + super().setUp() |
| 891 | + self.url = reverse("openedx_authz:orgs-list") |
| 892 | + |
| 893 | + def test_get_orgs_returns_all(self): |
| 894 | + """Test that all orgs are returned when no search param is provided. |
| 895 | +
|
| 896 | + Expected result: |
| 897 | + - Returns 200 OK status |
| 898 | + - Returns all 3 orgs |
| 899 | + """ |
| 900 | + response = self.client.get(self.url) |
| 901 | + |
| 902 | + self.assertEqual(response.status_code, status.HTTP_200_OK) |
| 903 | + self.assertEqual(response.data["count"], 3) |
| 904 | + self.assertEqual(len(response.data["results"]), 3) |
| 905 | + |
| 906 | + @data( |
| 907 | + # Match by name |
| 908 | + ("Alpha", 1), |
| 909 | + ("university", 1), |
| 910 | + # Match by short_name |
| 911 | + ("BetaI", 1), |
| 912 | + ("gamma", 1), |
| 913 | + # Partial match across multiple orgs |
| 914 | + ("a", 3), |
| 915 | + # No match |
| 916 | + ("nonexistent", 0), |
| 917 | + ) |
| 918 | + @unpack |
| 919 | + def test_get_orgs_search(self, search_term: str, expected_count: int): |
| 920 | + """Test filtering orgs by name or short_name via the search param. |
| 921 | +
|
| 922 | + Expected result: |
| 923 | + - Returns 200 OK status |
| 924 | + - Returns only orgs matching the search term |
| 925 | + """ |
| 926 | + response = self.client.get(self.url, {"search": search_term}) |
| 927 | + |
| 928 | + self.assertEqual(response.status_code, status.HTTP_200_OK) |
| 929 | + self.assertEqual(response.data["count"], expected_count) |
| 930 | + self.assertEqual(len(response.data["results"]), expected_count) |
| 931 | + |
| 932 | + @data( |
| 933 | + ({}, 3, False), |
| 934 | + ({"page": 1, "page_size": 2}, 2, True), |
| 935 | + ({"page": 2, "page_size": 2}, 1, False), |
| 936 | + ({"page": 1, "page_size": 3}, 3, False), |
| 937 | + ) |
| 938 | + @unpack |
| 939 | + def test_get_orgs_pagination(self, query_params: dict, expected_count: int, has_next: bool): |
| 940 | + """Test pagination of org results. |
| 941 | +
|
| 942 | + Expected result: |
| 943 | + - Returns 200 OK status |
| 944 | + - Returns correct page size and next link |
| 945 | + """ |
| 946 | + response = self.client.get(self.url, query_params) |
| 947 | + |
| 948 | + self.assertEqual(response.status_code, status.HTTP_200_OK) |
| 949 | + self.assertEqual(len(response.data["results"]), expected_count) |
| 950 | + if has_next: |
| 951 | + self.assertIsNotNone(response.data["next"]) |
| 952 | + else: |
| 953 | + self.assertIsNone(response.data["next"]) |
| 954 | + |
| 955 | + def test_get_orgs_response_shape(self): |
| 956 | + """Test that each org result contains the expected fields. |
| 957 | +
|
| 958 | + Expected result: |
| 959 | + - Each result has id, name, and short_name fields |
| 960 | + """ |
| 961 | + response = self.client.get(self.url) |
| 962 | + |
| 963 | + self.assertEqual(response.status_code, status.HTTP_200_OK) |
| 964 | + result = response.data["results"][0] |
| 965 | + self.assertIn("id", result) |
| 966 | + self.assertIn("name", result) |
| 967 | + self.assertIn("short_name", result) |
| 968 | + |
| 969 | + def test_get_orgs_excludes_inactive(self): |
| 970 | + """Test that inactive orgs are not returned. |
| 971 | +
|
| 972 | + Expected result: |
| 973 | + - Returns 200 OK status |
| 974 | + - Inactive orgs are excluded from results |
| 975 | + """ |
| 976 | + Organization.objects.create(name="Inactive Org", short_name="InactiveO", active=False) |
| 977 | + |
| 978 | + response = self.client.get(self.url) |
| 979 | + |
| 980 | + self.assertEqual(response.status_code, status.HTTP_200_OK) |
| 981 | + self.assertEqual(response.data["count"], 3) |
| 982 | + result_names = [org["name"] for org in response.data["results"]] |
| 983 | + self.assertNotIn("Inactive Org", result_names) |
| 984 | + |
| 985 | + @data( |
| 986 | + # Only VIEW_LIBRARY_TEAM (library_user role in a lib scope) |
| 987 | + ("regular_1", status.HTTP_200_OK), |
| 988 | + # Only COURSES_VIEW_COURSE_TEAM (course_staff role in a course scope) |
| 989 | + ("regular_9", status.HTTP_200_OK), |
| 990 | + # No relevant permissions |
| 991 | + ("regular_10", status.HTTP_403_FORBIDDEN), |
| 992 | + # Superuser |
| 993 | + ("admin_1", status.HTTP_200_OK), |
| 994 | + ) |
| 995 | + @unpack |
| 996 | + def test_get_orgs_permissions(self, username: str, expected_status: int): |
| 997 | + """Test access control for AdminConsoleOrgsAPIView. |
| 998 | +
|
| 999 | + Test cases: |
| 1000 | + - User with only VIEW_LIBRARY_TEAM (via library role): allowed |
| 1001 | + - User with only COURSES_VIEW_COURSE_TEAM (via course role): allowed |
| 1002 | + - User with neither permission: forbidden |
| 1003 | + - Superuser/staff: allowed |
| 1004 | +
|
| 1005 | + Expected result: |
| 1006 | + - Returns appropriate status code based on user permissions |
| 1007 | + """ |
| 1008 | + user = User.objects.get(username=username) |
| 1009 | + self.client.force_authenticate(user=user) |
| 1010 | + |
| 1011 | + response = self.client.get(self.url) |
| 1012 | + |
| 1013 | + self.assertEqual(response.status_code, expected_status) |
| 1014 | + |
| 1015 | + def test_get_orgs_user_with_both_permissions_allowed(self): |
| 1016 | + """Test that a user with both VIEW_LIBRARY_TEAM and COURSES_VIEW_COURSE_TEAM can access the endpoint. |
| 1017 | +
|
| 1018 | + Expected result: |
| 1019 | + - Returns 200 OK status |
| 1020 | + """ |
| 1021 | + # regular_1 has library_user (VIEW_LIBRARY_TEAM); assign a course role too |
| 1022 | + self._assign_roles_to_users( |
| 1023 | + [ |
| 1024 | + { |
| 1025 | + "subject_name": "regular_1", |
| 1026 | + "role_name": roles.COURSE_STAFF.external_key, |
| 1027 | + "scope_name": "course-v1:Org1+COURSE1+2024", |
| 1028 | + }, |
| 1029 | + ] |
| 1030 | + ) |
| 1031 | + user = User.objects.get(username="regular_1") |
| 1032 | + self.client.force_authenticate(user=user) |
| 1033 | + |
| 1034 | + response = self.client.get(self.url) |
| 1035 | + |
| 1036 | + self.assertEqual(response.status_code, status.HTTP_200_OK) |
| 1037 | + |
| 1038 | + def test_get_orgs_unauthenticated(self): |
| 1039 | + """Test that unauthenticated requests are rejected. |
| 1040 | +
|
| 1041 | + Expected result: |
| 1042 | + - Returns 401 UNAUTHORIZED status |
| 1043 | + """ |
| 1044 | + self.client.force_authenticate(user=None) |
| 1045 | + |
| 1046 | + response = self.client.get(self.url) |
| 1047 | + |
| 1048 | + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) |
| 1049 | + |
| 1050 | + |
856 | 1051 | @ddt |
857 | 1052 | class TestRoleListView(ViewTestMixin): |
858 | 1053 | """Test suite for RoleListView.""" |
|
0 commit comments