Skip to content

Commit 04f78d7

Browse files
committed
fix: Fix LTI based duplicate email users automatically
1 parent cbb5abb commit 04f78d7

File tree

4 files changed

+73
-12
lines changed

4 files changed

+73
-12
lines changed

openedx/api.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,14 @@ def _is_duplicate_username_error(resp, data):
113113
)
114114

115115

116+
def _is_duplicate_email_error(resp, data):
117+
"""Check if the response indicates a duplicate email error."""
118+
return (
119+
resp.status_code == status.HTTP_409_CONFLICT
120+
and data.get("error_code") == "duplicate-email"
121+
)
122+
123+
116124
def _is_bad_request(resp):
117125
"""Check if the response indicates a bad request."""
118126
return resp.status_code in (
@@ -257,7 +265,7 @@ def _set_edx_error(open_edx_user, data):
257265
open_edx_user.save()
258266

259267

260-
def _create_edx_user_request(open_edx_user, user, access_token):
268+
def _create_edx_user_request(open_edx_user, user, access_token): # noqa: C901, PLR0915
261269
"""
262270
Handle the actual user creation request to Open edX with retry logic for duplicate usernames.
263271
@@ -344,6 +352,16 @@ def _create_edx_user_request(open_edx_user, user, access_token):
344352
attempt = 0
345353
continue
346354
else:
355+
# Only try for LTI user duplicate email error if the response error was duplicate-email
356+
if _is_duplicate_email_error(resp, data):
357+
client = get_edx_api_lti_dup_email_client()
358+
dup_email_fix_resp = client.fix_lti_user(
359+
email=user.email, username=current_username
360+
)
361+
if dup_email_fix_resp.status_code == status.HTTP_200_OK:
362+
open_edx_user.has_been_synced = True
363+
open_edx_user.save()
364+
return True
347365
break
348366

349367
if _is_bad_request(resp):
@@ -923,6 +941,17 @@ def get_edx_api_course_detail_client():
923941
return edx_client.course_detail
924942

925943

944+
def get_edx_api_lti_dup_email_client():
945+
"""
946+
Gets an edx api client instance for use with the grades api
947+
948+
Returns:
949+
CourseDetails: edx api course client instance
950+
"""
951+
edx_client = get_edx_api_service_client()
952+
return edx_client.lti_tools
953+
954+
926955
def get_edx_api_course_mode_client():
927956
"""
928957
Gets an edx api client instance for use with the grades api

openedx/api_test.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,20 @@ def test_create_edx_user( # noqa: PLR0913
244244
},
245245
],
246246
)
247-
def test_create_edx_user_409_errors(settings, error_data):
248-
"""Test that create_edx_user handles a 409 response from the edX API"""
247+
@pytest.mark.parametrize(
248+
"lti_fix_response_status",
249+
[
250+
status.HTTP_200_OK,
251+
status.HTTP_400_BAD_REQUEST,
252+
status.HTTP_500_INTERNAL_SERVER_ERROR,
253+
],
254+
)
255+
def test_create_edx_user_409_errors(settings, error_data, lti_fix_response_status):
256+
"""Test that create_edx_user handles a 409 response from the edX API
257+
1. If the error is duplicate-email, it should call the LTI fix endpoint
258+
2. If the LTI fix endpoint returns 200, the user should be marked as synced
259+
3. If the LTI fix endpoint returns non-200, the user should be marked as having a sync error
260+
"""
249261
user = UserFactory.create(
250262
openedx_user__has_been_synced=False,
251263
)
@@ -262,19 +274,35 @@ def test_create_edx_user_409_errors(settings, error_data):
262274
json=error_data,
263275
status=status.HTTP_409_CONFLICT,
264276
)
277+
resp3 = responses.add(
278+
responses.POST,
279+
f"{settings.OPENEDX_API_BASE_URL}/api/lti-user-fix/",
280+
json={},
281+
status=lti_fix_response_status,
282+
)
265283

266284
create_edx_user(user)
267285

268286
assert resp1.call_count == 0
269287
assert resp2.call_count == 1
288+
is_duplicate_email = error_data.get("error_code") == "duplicate-email"
289+
if is_duplicate_email:
290+
assert resp3.call_count == 1
291+
else:
292+
assert resp3.call_count == 0
270293

271294
user.refresh_from_db()
272295

273296
edx_user = user.openedx_users.first()
274297

275-
assert edx_user.has_been_synced is False
276-
assert edx_user.has_sync_error is True
277-
assert edx_user.sync_error_data == error_data
298+
if lti_fix_response_status == status.HTTP_200_OK and is_duplicate_email:
299+
assert edx_user.has_been_synced is True
300+
assert edx_user.has_sync_error is False
301+
assert edx_user.sync_error_data is None
302+
else:
303+
assert edx_user.has_been_synced is False
304+
assert edx_user.has_sync_error is True
305+
assert edx_user.sync_error_data == error_data
278306

279307

280308
@responses.activate

poetry.lock

Lines changed: 9 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ djangorestframework = "^3.12.4"
4242
djoser = "^2.1.0"
4343
drf-extensions = "^0.8.0"
4444
drf-spectacular = "^0.28.0"
45-
edx-api-client = "^1.13.0"
45+
edx-api-client = { git = "https://github.com/mitodl/edx-api-client", branch = "arslan/8853-fix-lti-dup-users" }
4646
hubspot-api-client = "^6.1.0"
4747
ipython = "^8.0.0"
4848
mitol-django-apigateway = "2025.8.14"

0 commit comments

Comments
 (0)