From e17a9006c62a196777be5073a9f2076bd4916755 Mon Sep 17 00:00:00 2001 From: Alexander Dusenbery Date: Thu, 20 Feb 2025 16:22:38 -0500 Subject: [PATCH] feat: basics of business logic for customer provisioning --- .../apps/api/v1/views/provisioning.py | 28 +++++++- .../apps/api_client/lms_client.py | 27 ++++++++ .../apps/api_client/tests/test_lms_client.py | 62 ++++++++++++++++++ .../apps/provisioning/__init__.py | 0 enterprise_access/apps/provisioning/api.py | 64 +++++++++++++++++++ 5 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 enterprise_access/apps/provisioning/__init__.py create mode 100644 enterprise_access/apps/provisioning/api.py diff --git a/enterprise_access/apps/api/v1/views/provisioning.py b/enterprise_access/apps/api/v1/views/provisioning.py index df0f182e..3feec951 100644 --- a/enterprise_access/apps/api/v1/views/provisioning.py +++ b/enterprise_access/apps/api/v1/views/provisioning.py @@ -11,6 +11,7 @@ from enterprise_access.apps.api import serializers from enterprise_access.apps.core import constants +from enterprise_access.apps.provisioning import api as provisioning_api logger = logging.getLogger(__name__) @@ -37,4 +38,29 @@ class ProvisioningCreateView(PermissionRequiredMixin, generics.CreateAPIView): def create(self, request, *args, **kwargs): request_serializer = serializers.ProvisioningRequestSerializer(data=request.data) request_serializer.is_valid(raise_exception=True) - return Response('ack', status=status.HTTP_201_CREATED) + + customer_request_data = request_serializer.validated_data['enterprise_customer'] + created_customer = provisioning_api.get_or_create_enterprise_customer( + name=customer_request_data['name'], + country=customer_request_data['country'], + slug=customer_request_data['slug'], + ) + + admin_emails = [ + record.get('user_email') + for record in request_serializer.validated_data['pending_admins'] + ] + + customer_admins = provisioning_api.get_or_create_enterprise_admin_users( + enterprise_customer_uuid=created_customer['uuid'], + user_emails=admin_emails, + ) + + response_serializer = serializers.ProvisioningResponseSerializer({ + 'enterprise_customer': created_customer, + 'pending_admins': customer_admins, + }) + return Response( + response_serializer.data, + status=status.HTTP_201_CREATED, + ) diff --git a/enterprise_access/apps/api_client/lms_client.py b/enterprise_access/apps/api_client/lms_client.py index 5c54eff8..78fdfb75 100755 --- a/enterprise_access/apps/api_client/lms_client.py +++ b/enterprise_access/apps/api_client/lms_client.py @@ -195,6 +195,33 @@ def get_enterprise_admin_users(self, enterprise_customer_uuid): return results + def get_enterprise_pending_admin_users(self, enterprise_customer_uuid): + """ + Gets all pending enterprise admin records for the given customer uuid. + + Arguments: + enterprise_customer_uuid (UUID): UUID of the enterprise customer. + Returns: + List of dictionaries of pending admin users. + """ + try: + response = self.client.get( + self.pending_enterprise_admin_endpoint + f'?enterprise_customer={enterprise_customer_uuid}', + timeout=settings.LMS_CLIENT_TIMEOUT, + ) + response.raise_for_status() + logger.info( + 'Fetched pending admin records for customer %s', enterprise_customer_uuid, + ) + payload = response.json() + return payload.get('results', []) + except requests.exceptions.HTTPError: + logger.exception( + 'Failed to fetch pending admin record for customer %s', + enterprise_customer_uuid, + ) + raise + def create_enterprise_admin_user(self, enterprise_customer_uuid, user_email): """ Creates a new enterprise pending admin record. diff --git a/enterprise_access/apps/api_client/tests/test_lms_client.py b/enterprise_access/apps/api_client/tests/test_lms_client.py index d5584d63..7f990a0e 100644 --- a/enterprise_access/apps/api_client/tests/test_lms_client.py +++ b/enterprise_access/apps/api_client/tests/test_lms_client.py @@ -216,6 +216,43 @@ def test_create_enterprise_customer_data(self, mock_oauth_client, mock_json): timeout=settings.LMS_CLIENT_TIMEOUT, ) + @mock.patch('requests.Response.json') + @mock.patch('enterprise_access.apps.api_client.base_oauth.OAuthAPIClient') + def test_get_enterprise_pending_admin_users(self, mock_oauth_client, mock_json): + """ + Test that we can use the LmsApiClient to fetch existing pending admin records. + """ + customer_uuid = str(uuid4()) + + mock_response_payload_results = [{ + 'id': 1, + 'enterprise_customer': customer_uuid, + 'user_email': 'test-existing-admin@example.com', + }] + mock_response_payload = { + 'count': 1, + 'results': mock_response_payload_results, + } + mock_json.return_value = mock_response_payload + + mock_get = mock_oauth_client.return_value.get + + mock_get.return_value = requests.Response() + mock_get.return_value.status_code = 200 + + client = LmsApiClient() + response_payload = client.get_enterprise_pending_admin_users(customer_uuid) + + self.assertEqual(response_payload, mock_response_payload_results) + expected_url = ( + 'http://edx-platform.example.com/enterprise/api/v1/pending-enterprise-admin/' + f'?enterprise_customer={customer_uuid}' + ) + mock_get.assert_called_once_with( + expected_url, + timeout=settings.LMS_CLIENT_TIMEOUT, + ) + @mock.patch('requests.Response.json') @mock.patch('enterprise_access.apps.api_client.base_oauth.OAuthAPIClient') def test_create_enterprise_admin_user(self, mock_oauth_client, mock_json): @@ -316,6 +353,31 @@ def test_create_enterprise_admin_error(self, mock_oauth_client): timeout=settings.LMS_CLIENT_TIMEOUT, ) + @mock.patch('enterprise_access.apps.api_client.base_oauth.OAuthAPIClient') + def test_get_enterprise_pending_admin_error(self, mock_oauth_client): + """ + Tests that we raise an exception appropriately when listing pending + admin records with the LmsApiClient(). + """ + customer_uuid = str(uuid4()) + mock_get = mock_oauth_client.return_value.get + + mock_get.side_effect = requests.exceptions.HTTPError('whoopsie') + mock_get.return_value.status_code = 400 + + client = LmsApiClient() + with self.assertRaises(requests.exceptions.HTTPError): + client.get_enterprise_pending_admin_users(customer_uuid) + + expected_url = ( + 'http://edx-platform.example.com/enterprise/api/v1/pending-enterprise-admin/' + f'?enterprise_customer={customer_uuid}' + ) + mock_get.assert_called_once_with( + expected_url, + timeout=settings.LMS_CLIENT_TIMEOUT, + ) + @mock.patch('enterprise_access.apps.api_client.base_oauth.OAuthAPIClient') def test_unlink_users_from_enterprise(self, mock_oauth_client): """ diff --git a/enterprise_access/apps/provisioning/__init__.py b/enterprise_access/apps/provisioning/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/enterprise_access/apps/provisioning/api.py b/enterprise_access/apps/provisioning/api.py new file mode 100644 index 00000000..14e72db0 --- /dev/null +++ b/enterprise_access/apps/provisioning/api.py @@ -0,0 +1,64 @@ +""" +Python API for provisioning operations. +""" +import logging + +from ..api_client.lms_client import LmsApiClient + +logger = logging.getLogger(__name__) + + +def get_or_create_enterprise_customer(*, name, slug, country, **kwargs): + """ + Get or creates an enterprise customer with the provided arguments. + """ + client = LmsApiClient() + existing_customer = client.get_enterprise_customer_data(enterprise_customer_slug=slug) + if existing_customer: + logger.info('Provisioning: enterprise_customer slug %s already exists', slug) + return existing_customer + + created_customer = client.create_enterprise_customer( + name=name, slug=slug, country=country, **kwargs, + ) + logger.info('Provisioning: created enterprise customer with slug %s', slug) + + +def get_or_create_enterprise_admin_users(enterprise_customer_uuid, user_emails): + """ + Creates pending admin records from the given ``user_email`` for the customer + identified by ``enterprise_customer_uuid``. + """ + client = LmsApiClient() + existing_admins = client.get_enterprise_admin_users(enterprise_customer_uuid) + existing_admin_emails = {record['email'] for record in existing_admins} + logger.info( + 'Provisioning: customer %s has existing admin emails %s', + enterprise_customer_uuid, + existing_admin_emails, + ) + + existing_pending_admins = client.get_enterprise_pending_admin_users(enterprise_customer_uuid) + existing_pending_admin_emails = {record['user_email'] for record in existing_pending_admins} + logger.info( + 'Provisioning: customer %s has existing pending admin emails %s', + enterprise_customer_uuid, + existing_pending_admin_emails, + ) + + user_emails_to_create = list( + (set(user_emails) - existing_admin_emails) - existing_pending_admin_emails + ) + + created_admins = [] + for user_email in user_emails_to_create: + created_admins.append( + client.create_enterprise_admin_user(enterprise_customer_uuid, user_email) + ) + logger.info( + 'Provisioning: created admin %s for customer %s', + user_email, + enterprise_customer_uuid, + ) + + return created_admins + existing_pending_admins