diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 05d596f68..b7dd2442c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,10 @@ Unreleased ---------- * nothing unreleased +[6.6.9] - 2026-03-10 +--------------------- +* fix: handle duplicate enterprise group name validation error (ENT-11506) + [6.6.8] - 2026-03-05 --------------------- * feat: moving retirement code to edx-enterprise diff --git a/enterprise/__init__.py b/enterprise/__init__.py index d3581b2d0..35ec3f5af 100644 --- a/enterprise/__init__.py +++ b/enterprise/__init__.py @@ -2,4 +2,4 @@ Your project description goes here. """ -__version__ = "6.6.8" +__version__ = "6.6.9" diff --git a/enterprise/api/v1/serializers.py b/enterprise/api/v1/serializers.py index 5de636e0e..4388e3463 100644 --- a/enterprise/api/v1/serializers.py +++ b/enterprise/api/v1/serializers.py @@ -12,6 +12,7 @@ from rest_framework import serializers from rest_framework.fields import empty from rest_framework.settings import api_settings +from rest_framework.validators import UniqueTogetherValidator from slumber.exceptions import HttpClientError from django.contrib import auth @@ -795,6 +796,13 @@ class Meta: fields = ( 'enterprise_customer', 'name', 'uuid', 'accepted_members_count', 'group_type', 'created') + validators = [ + UniqueTogetherValidator( + queryset=models.EnterpriseGroup.all_objects.all(), + fields=('name', 'enterprise_customer'), + message='A group with this name already exists. Please enter a unique name to create a new group.', + ) + ] accepted_members_count = serializers.SerializerMethodField() diff --git a/tests/test_enterprise/api/test_serializers.py b/tests/test_enterprise/api/test_serializers.py index 93c03bf92..20867ffe0 100644 --- a/tests/test_enterprise/api/test_serializers.py +++ b/tests/test_enterprise/api/test_serializers.py @@ -25,6 +25,7 @@ EnterpriseCustomerReportingConfigurationSerializer, EnterpriseCustomerSerializer, EnterpriseCustomerUserReadOnlySerializer, + EnterpriseGroupSerializer, EnterpriseMembersSerializer, EnterpriseSSOUserInfoRequestSerializer, EnterpriseUserSerializer, @@ -188,6 +189,52 @@ def test_serialize_auth_org_id(self): self.assertEqual(serialized_auth_org_id, expected_auth_org_id) +@mark.django_db +class TestEnterpriseGroupSerializer(APITest): + """ + Tests for EnterpriseGroupSerializer. + """ + + def setUp(self): + super().setUp() + self.enterprise_customer = factories.EnterpriseCustomerFactory() + self.group_name = 'duplicate-group-name' + self.duplicate_name_error = ( + 'A group with this name already exists. Please enter a unique name to create a new group.' + ) + + def _serialize_group(self): + """Return a serializer with duplicate-prone payload.""" + return EnterpriseGroupSerializer(data={ + 'enterprise_customer': self.enterprise_customer.uuid, + 'name': self.group_name, + 'group_type': 'flex', + }) + + def test_duplicate_group_name_returns_custom_error_message(self): + """Ensure active duplicate group names get the expected friendly validation message.""" + factories.EnterpriseGroupFactory( + enterprise_customer=self.enterprise_customer, + name=self.group_name, + ) + + serializer = self._serialize_group() + assert not serializer.is_valid() + assert serializer.errors.get('non_field_errors') == [self.duplicate_name_error] + + def test_deleted_duplicate_group_name_returns_custom_error_message(self): + """Ensure soft-deleted duplicate group names are also blocked with the same validation message.""" + group = factories.EnterpriseGroupFactory( + enterprise_customer=self.enterprise_customer, + name=self.group_name, + ) + group.delete() + + serializer = self._serialize_group() + assert not serializer.is_valid() + assert serializer.errors.get('non_field_errors') == [self.duplicate_name_error] + + @mark.django_db class TestEnterpriseCustomerMembersEndpointLearnersOnly(APITest): """ diff --git a/tests/test_enterprise/api/test_views.py b/tests/test_enterprise/api/test_views.py index f709191e6..52040aec6 100644 --- a/tests/test_enterprise/api/test_views.py +++ b/tests/test_enterprise/api/test_views.py @@ -8754,6 +8754,52 @@ def test_successful_post_group(self): assert response.json().get('name') == 'foobar' assert len(EnterpriseGroup.objects.filter(name='foobar')) == 1 + def test_duplicate_group_name_returns_custom_error(self): + """ + Test that creating a group with a duplicate name for the same enterprise + returns a 400 with a user-friendly error message. + """ + url = settings.TEST_SERVER + reverse('enterprise-group-list') + request_data = { + 'enterprise_customer': str(self.enterprise_customer.uuid), + 'name': 'duplicate-test-group', + } + # Create the first group + response = self.client.post(url, data=request_data) + assert response.status_code == 201 + assert response.json().get('name') == 'duplicate-test-group' + + # Attempt to create a second group with the same name and enterprise + duplicate_response = self.client.post(url, data=request_data) + assert duplicate_response.status_code == 400 + assert 'non_field_errors' in duplicate_response.json() + assert duplicate_response.json()['non_field_errors'] == [ + 'A group with this name already exists. Please enter a unique name to create a new group.', + ] + + def test_duplicate_group_name_different_enterprise_succeeds(self): + """ + Test that creating a group with the same name but different enterprise succeeds. + """ + url = settings.TEST_SERVER + reverse('enterprise-group-list') + # Create group for first enterprise + request_data_1 = { + 'enterprise_customer': str(self.enterprise_customer.uuid), + 'name': 'shared-group-name', + } + response_1 = self.client.post(url, data=request_data_1) + assert response_1.status_code == 201 + + # Create group with same name for a different enterprise + new_customer = EnterpriseCustomerFactory() + self.set_jwt_cookie(ENTERPRISE_ADMIN_ROLE, new_customer.pk) + request_data_2 = { + 'enterprise_customer': str(new_customer.uuid), + 'name': 'shared-group-name', + } + response_2 = self.client.post(url, data=request_data_2) + assert response_2.status_code == 201 + def test_successful_update_group(self): """ Test patching an existing group record