Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion enterprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Your project description goes here.
"""

__version__ = "6.6.8"
__version__ = "6.6.9"
8 changes: 8 additions & 0 deletions enterprise/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down
47 changes: 47 additions & 0 deletions tests/test_enterprise/api/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
EnterpriseCustomerReportingConfigurationSerializer,
EnterpriseCustomerSerializer,
EnterpriseCustomerUserReadOnlySerializer,
EnterpriseGroupSerializer,
EnterpriseMembersSerializer,
EnterpriseSSOUserInfoRequestSerializer,
EnterpriseUserSerializer,
Expand Down Expand Up @@ -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):
"""
Expand Down
46 changes: 46 additions & 0 deletions tests/test_enterprise/api/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down