Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: basics of business logic for customer provisioning #645

Merged
merged 1 commit into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions enterprise_access/apps/api/serializers/provisioning.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,32 @@ class EnterpriseCustomerResponseSerializer(BaseSerializer):
slug = serializers.SlugField(required=True, allow_blank=False)


class PendingCustomerAdminResponseSerializer(BaseSerializer):
class CreatedCustomerAdminResponseSerializer(BaseSerializer):
"""
Pending admin serializer for provisioning responses.
"""
user_email = serializers.EmailField()


class ExistingCustomerAdminResponseSerializer(BaseSerializer):
"""
Existing admin serializer for provisioning responses.
"""
user_email = serializers.EmailField()


class AdminObjectResponseSerializer(BaseSerializer):
"""
Container serializer to describe created and existing
admin emails in a provisioning response.
"""
created_admins = CreatedCustomerAdminResponseSerializer(many=True)
existing_admins = ExistingCustomerAdminResponseSerializer(many=True)


class ProvisioningResponseSerializer(BaseSerializer):
"""
Response serializer for provisioning create view.
"""
enterprise_customer = EnterpriseCustomerResponseSerializer()
pending_admins = PendingCustomerAdminResponseSerializer(many=True)
customer_admins = AdminObjectResponseSerializer()
282 changes: 272 additions & 10 deletions enterprise_access/apps/api/v1/tests/test_provisioning_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Tests for the provisioning views.
"""
import uuid
from unittest import mock

import ddt
from edx_rbac.constants import ALL_ACCESS_CONTEXT
Expand Down Expand Up @@ -43,11 +44,6 @@ class TestProvisioningAuth(APITest):
{'system_wide_role': SYSTEM_ENTERPRISE_ADMIN_ROLE, 'context': ALL_ACCESS_CONTEXT},
status.HTTP_403_FORBIDDEN,
),
# Even operators can't provision
(
{'system_wide_role': SYSTEM_ENTERPRISE_OPERATOR_ROLE, 'context': ALL_ACCESS_CONTEXT},
status.HTTP_403_FORBIDDEN,
),
# No JWT based auth, no soup for you.
(
None,
Expand All @@ -66,15 +62,26 @@ def test_provisioning_create_view_forbidden(self, role_context_dict, expected_re
response = self.client.post(PROVISIONING_CREATE_ENDPOINT)
assert response.status_code == expected_response_code

def test_provisioning_create_allowed_for_provisioning_admins(self):
@ddt.data(
(
{'system_wide_role': SYSTEM_ENTERPRISE_OPERATOR_ROLE, 'context': ALL_ACCESS_CONTEXT},
status.HTTP_201_CREATED,
),
(
{'system_wide_role': SYSTEM_ENTERPRISE_PROVISIONING_ADMIN_ROLE, 'context': ALL_ACCESS_CONTEXT},
status.HTTP_201_CREATED,
),
)
@ddt.unpack
@mock.patch('enterprise_access.apps.api.v1.views.provisioning.provisioning_api')
def test_provisioning_create_allowed_for_provisioning_admins(
self, role_context_dict, expected_response_code, mock_provisioning_api,
):
"""
Tests that we get expected 200 response for the provisioning create view when
the requesting user has the correct system role and provides a valid request payload.
"""
self.set_jwt_cookie([{
'system_wide_role': SYSTEM_ENTERPRISE_PROVISIONING_ADMIN_ROLE,
'context': ALL_ACCESS_CONTEXT,
}])
self.set_jwt_cookie([role_context_dict])

request_payload = {
"enterprise_customer": {
Expand All @@ -89,4 +96,259 @@ def test_provisioning_create_allowed_for_provisioning_admins(self):
],
}
response = self.client.post(PROVISIONING_CREATE_ENDPOINT, data=request_payload)
assert response.status_code == expected_response_code

mock_provisioning_api.get_or_create_enterprise_customer.assert_called_once_with(
**request_payload['enterprise_customer'],
)

created_customer = mock_provisioning_api.get_or_create_enterprise_customer.return_value
mock_provisioning_api.get_or_create_enterprise_admin_users.assert_called_once_with(
enterprise_customer_uuid=created_customer['uuid'],
user_emails=['[email protected]'],
)


@ddt.ddt
class TestProvisioningEndToEnd(APITest):
"""
Tests end-to-end calls to provisioning endpoints through mocked-out calls
to downstream services.
"""
def setUp(self):
super().setUp()
self.set_jwt_cookie([
{
'system_wide_role': SYSTEM_ENTERPRISE_PROVISIONING_ADMIN_ROLE,
'context': ALL_ACCESS_CONTEXT,
},
])

@ddt.data(
# Data representing the state where a net-new customer is created.
{
'existing_customer_data': None,
'created_customer_data': {
'name': 'Test Customer',
'slug': 'test-customer',
'country': 'US',
'uuid': TEST_ENTERPRISE_UUID,
},
'expected_get_customer_kwargs': {
'enterprise_customer_slug': 'test-customer',
},
'create_customer_called': True,
'expected_create_customer_kwargs': {
'name': 'Test Customer',
'slug': 'test-customer',
'country': 'US',
},
},
# Data representing the state where a customer with the given slug exists.
{
'existing_customer_data': {
'name': 'Test Customer',
'slug': 'test-customer',
'country': 'US',
'uuid': TEST_ENTERPRISE_UUID,
},
'created_customer_data': None,
'expected_get_customer_kwargs': {
'enterprise_customer_slug': 'test-customer',
},
'create_customer_called': False,
'expected_create_customer_kwargs': None
},
)
@mock.patch('enterprise_access.apps.provisioning.api.LmsApiClient')
def test_get_or_create_customer_and_admins_created(self, test_data, mock_lms_api_client):
"""
Tests cases where admins don't exist and customer is fetched or created.
"""
mock_client = mock_lms_api_client.return_value
mock_client.get_enterprise_customer_data.return_value = test_data['existing_customer_data']
mock_client.get_enterprise_admin_users.return_value = []
mock_client.get_enterprise_pending_admin_users.return_value = []

if test_data['created_customer_data']:
mock_client.create_enterprise_customer.return_value = test_data['created_customer_data']

mock_client.create_enterprise_admin_user.side_effect = [
{'user_email': '[email protected]', 'enterprise_customer_uuid': TEST_ENTERPRISE_UUID},
{'user_email': '[email protected]', 'enterprise_customer_uuid': TEST_ENTERPRISE_UUID},
]

request_payload = {
"enterprise_customer": {
'name': 'Test Customer',
'slug': 'test-customer',
'country': 'US',
},
'pending_admins': [
{'user_email': '[email protected]'},
{'user_email': '[email protected]'},
],
}
response = self.client.post(PROVISIONING_CREATE_ENDPOINT, data=request_payload)
assert response.status_code == status.HTTP_201_CREATED

expected_response_payload = {
'enterprise_customer': {
'name': 'Test Customer',
'slug': 'test-customer',
'country': 'US',
'uuid': str(TEST_ENTERPRISE_UUID),
},
'customer_admins': {
'created_admins': [
{'user_email': '[email protected]'},
{'user_email': '[email protected]'},
],
'existing_admins': [],
},
}
actual_response_payload = response.json()
self.assertEqual(actual_response_payload, expected_response_payload)

mock_client.get_enterprise_customer_data.assert_called_once_with(
**test_data['expected_get_customer_kwargs'],
)
if test_data['create_customer_called']:
mock_client.create_enterprise_customer.assert_called_once_with(
**test_data['expected_create_customer_kwargs'],
)
else:
self.assertFalse(mock_client.create_enterprise_customer.called)

mock_client.get_enterprise_admin_users.assert_called_once_with(TEST_ENTERPRISE_UUID)
mock_client.get_enterprise_pending_admin_users.assert_called_once_with(TEST_ENTERPRISE_UUID)
mock_client.create_enterprise_admin_user.assert_has_calls([
mock.call(TEST_ENTERPRISE_UUID, '[email protected]'),
mock.call(TEST_ENTERPRISE_UUID, '[email protected]'),
], any_order=True)

@ddt.data(
# No admin users exist, two pending admins created.
{
'existing_admin_users': [],
'existing_pending_admin_users': [],
'create_pending_admins_called': True,
'create_admin_user_side_effect': [
{'user_email': '[email protected]', 'enterprise_customer_uuid': TEST_ENTERPRISE_UUID},
{'user_email': '[email protected]', 'enterprise_customer_uuid': TEST_ENTERPRISE_UUID},
],
'expected_create_pending_admin_calls': [
mock.call(TEST_ENTERPRISE_UUID, '[email protected]'),
mock.call(TEST_ENTERPRISE_UUID, '[email protected]'),
],
},
# One pending admin exists, one new one created.
{
'existing_admin_users': [],
'existing_pending_admin_users': [
{'user_email': '[email protected]', 'enterprise_customer_uuid': TEST_ENTERPRISE_UUID},
],
'create_pending_admins_called': True,
'create_admin_user_side_effect': [
{'user_email': '[email protected]', 'enterprise_customer_uuid': TEST_ENTERPRISE_UUID},
],
'expected_create_pending_admin_calls': [
mock.call(TEST_ENTERPRISE_UUID, '[email protected]'),
],
},
# One full admin exists, one new pending admin created.
{
'existing_admin_users': [
{'email': '[email protected]', 'enterprise_customer_uuid': TEST_ENTERPRISE_UUID},
],
'existing_pending_admin_users': [],
'create_pending_admins_called': True,
'create_admin_user_side_effect': [
{'user_email': '[email protected]', 'enterprise_customer_uuid': TEST_ENTERPRISE_UUID},
],
'expected_create_pending_admin_calls': [
mock.call(TEST_ENTERPRISE_UUID, '[email protected]'),
],
},
# One full admin exists, one pending exists, none created.
{
'existing_admin_users': [
{'email': '[email protected]', 'enterprise_customer_uuid': TEST_ENTERPRISE_UUID},
],
'existing_pending_admin_users': [
{'user_email': '[email protected]', 'enterprise_customer_uuid': TEST_ENTERPRISE_UUID},
],
'create_pending_admins_called': False,
'create_admin_user_side_effect': [],
'expected_create_pending_admin_calls': [],
},
)
@mock.patch('enterprise_access.apps.provisioning.api.LmsApiClient')
def test_customer_fetched_admins_fetched_or_created(self, test_data, mock_lms_api_client):
"""
Tests cases where [pending]admins are fetched or created, but the customer
already exists
"""
mock_client = mock_lms_api_client.return_value
mock_client.get_enterprise_customer_data.return_value = {
'name': 'Test Customer',
'slug': 'test-customer',
'country': 'US',
'uuid': TEST_ENTERPRISE_UUID,
}
mock_client.get_enterprise_admin_users.return_value = test_data['existing_admin_users']
mock_client.get_enterprise_pending_admin_users.return_value = test_data['existing_pending_admin_users']
mock_client.create_enterprise_admin_user.side_effect = test_data['create_admin_user_side_effect']

request_payload = {
'enterprise_customer': {
'name': 'Test Customer',
'slug': 'test-customer',
'country': 'US',
},
'pending_admins': [
{'user_email': '[email protected]'},
{'user_email': '[email protected]'},
],
}
response = self.client.post(PROVISIONING_CREATE_ENDPOINT, data=request_payload)
assert response.status_code == status.HTTP_201_CREATED

existing_emails = sorted(
[record['email'] for record in test_data['existing_admin_users']] +
[record['user_email'] for record in test_data['existing_pending_admin_users']]
)
expected_existing_admins = [{'user_email': email} for email in existing_emails]
expected_created_admins = [
{'user_email': record['user_email']}
for record in test_data['create_admin_user_side_effect']
]
expected_response_payload = {
'enterprise_customer': {
'name': 'Test Customer',
'slug': 'test-customer',
'country': 'US',
'uuid': str(TEST_ENTERPRISE_UUID),
},
'customer_admins': {
'created_admins': expected_created_admins,
'existing_admins': expected_existing_admins,
},
}
actual_response_payload = response.json()
self.assertEqual(actual_response_payload, expected_response_payload)

mock_client.get_enterprise_customer_data.assert_called_once_with(
enterprise_customer_slug='test-customer',
)
self.assertFalse(mock_client.create_enterprise_customer.called)

mock_client.get_enterprise_admin_users.assert_called_once_with(TEST_ENTERPRISE_UUID)
mock_client.get_enterprise_pending_admin_users.assert_called_once_with(TEST_ENTERPRISE_UUID)
if test_data['create_pending_admins_called']:
mock_client.create_enterprise_admin_user.assert_has_calls(
test_data['expected_create_pending_admin_calls'],
any_order=True,
)
else:
self.assertFalse(mock_client.create_enterprise_admin_user.called)
Loading