Skip to content

Commit 9ff3d6b

Browse files
committed
refactor
1 parent 3a4deee commit 9ff3d6b

File tree

2 files changed

+54
-127
lines changed

2 files changed

+54
-127
lines changed

src/ol_openedx_lti_utilities/ol_openedx_lti_utilities/views.py

Lines changed: 30 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,26 @@
33
"""
44

55
import logging
6-
import re
76

8-
from django.contrib.auth.models import User
9-
from django.http import Http404
7+
from django.http import Http404, HttpResponseBadRequest
108
from edx_rest_framework_extensions import permissions
119
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
1210
from edx_rest_framework_extensions.auth.session.authentication import (
1311
SessionAuthenticationAllowInactiveUser,
1412
)
1513
from lms.djangoapps.lti_provider.models import LtiUser
14+
from openedx.core.djangoapps.user_api.accounts.utils import (
15+
create_retirement_request_and_deactivate_account,
16+
)
1617
from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser
1718
from rest_framework import status
1819
from rest_framework.response import Response
1920
from rest_framework.views import APIView
2021

2122
log = logging.getLogger(__name__)
2223

23-
# Matches 40-character random username used by LTI users
24-
LTI_USERNAME_REGEX = r"^[A-Za-z0-9]{40}$"
24+
25+
PLACEHOLDER_EMAIL_DOMAIN = "lti_example.com"
2526

2627

2728
class LtiUserFixView(APIView):
@@ -33,7 +34,6 @@ class LtiUserFixView(APIView):
3334
Request payload:
3435
{
3536
"email": "<user_email>",
36-
"username": "<desired_username>"
3737
}
3838
3939
Responses:
@@ -79,45 +79,32 @@ def post(self, request):
7979
If no LTI user exists for the provided email address
8080
"""
8181
user_email = request.data.get("email")
82-
user_username = request.data.get("username")
8382

84-
if not user_email or not user_username:
85-
log.error("email and username are required")
86-
return Response(
87-
{"detail": "email and username are required"},
88-
status=status.HTTP_400_BAD_REQUEST,
83+
if not user_email:
84+
log.error("email is required")
85+
return HttpResponseBadRequest("email is required")
86+
87+
# A user that is created by LTI will always have the same username as
88+
# lti_user_id in LtiUser table.
89+
lti_user = LtiUser.objects.filter(edx_user__email=user_email).first()
90+
if not lti_user:
91+
log.error("No user was found against the given email (%s)", user_email)
92+
raise Http404
93+
if lti_user.lti_user_id != lti_user.edx_user.username:
94+
log.error(
95+
"User with email (%s) does not appear to be an LTI-created user",
96+
user_email,
8997
)
90-
91-
def get_bad_lti_user_or_raise(email):
92-
"""Get LTI user or raise if none exists."""
93-
qs = LtiUser.objects.filter(edx_user__email=email)
94-
if not qs.exists():
95-
raise LtiUser.DoesNotExist
96-
return qs.first()
97-
98-
try:
99-
lti_user = get_bad_lti_user_or_raise(user_email)
100-
101-
# LTI-created users have a 40-char random username → "bad" username
102-
bad_lti_user = (
103-
lti_user.edx_user
104-
if re.match(LTI_USERNAME_REGEX, lti_user.edx_user.username)
105-
else None
98+
return HttpResponseBadRequest(
99+
"User with the given email does not appear to be an LTI-created user."
106100
)
107-
108-
if not bad_lti_user:
109-
return Response(
110-
{"detail": "User does not need fixing"},
111-
status=status.HTTP_400_BAD_REQUEST,
112-
)
113-
114-
except LtiUser.DoesNotExist as exc:
115-
log.error("No user was found against the given email (%s)", user_email) # noqa: TRY400
116-
raise Http404 from exc
117-
118-
# Fix username
119-
user = User.objects.get(email=user_email)
120-
user.username = user_username
101+
# Fix username of the LTI created user
102+
user = lti_user.edx_user
103+
user.email = user.email.split("@")[0] + "@" + PLACEHOLDER_EMAIL_DOMAIN
121104
user.save()
122-
105+
# Send the user for retirement and deactivate the account
106+
try:
107+
create_retirement_request_and_deactivate_account(user)
108+
except Exception as e: # noqa: BLE001
109+
log.error("Error retiring and deactivating user: %s", e) # noqa: TRY400
123110
return Response(status=status.HTTP_200_OK)

src/ol_openedx_lti_utilities/tests/test_views.py

Lines changed: 24 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from ddt import data, ddt, unpack
44
from django.contrib.auth.models import User
55
from django.test import TestCase
6-
from lms.djangoapps.lti_provider.models import LtiUser
76
from rest_framework import status
87
from rest_framework.test import APIClient
98

@@ -36,25 +35,25 @@ def setUp(self):
3635
def test_successful_lti_user_fix(self, mock_user_model, mock_lti_user_model):
3736
"""Test successful LTI user fix."""
3837
# Mock LTI user with bad username
38+
mock_lti_user = Mock()
39+
mock_lti_user.lti_user_id = "TEST_LTI_USER_ID"
40+
3941
mock_edx_user = Mock()
40-
mock_edx_user.username = self.lti_username
42+
mock_edx_user.username = "TEST_LTI_USER_ID"
4143
mock_edx_user.email = self.valid_email
42-
43-
mock_lti_user = Mock()
4444
mock_lti_user.edx_user = mock_edx_user
4545

4646
mock_qs = Mock()
47-
mock_qs.exists.return_value = True
4847
mock_qs.first.return_value = mock_lti_user
4948
mock_lti_user_model.objects.filter.return_value = mock_qs
5049

5150
# Mock User.objects.get
5251
mock_user = Mock()
5352
mock_user_model.objects.get.return_value = mock_user
5453

55-
data = {"email": self.valid_email, "username": self.valid_username}
54+
payload = {"email": self.valid_email, "username": self.valid_username}
5655

57-
response = self.client.post(self.url, data)
56+
response = self.client.post(self.url, payload)
5857

5958
assert response.status_code == status.HTTP_200_OK
6059
mock_user_model.objects.get.assert_called_once_with(email=self.valid_email)
@@ -69,23 +68,19 @@ def test_successful_lti_user_fix(self, mock_user_model, mock_lti_user_model):
6968
def test_missing_required_fields(self, test_data, expected_message):
7069
"""Test request with missing required fields."""
7170
response = self.client.post(self.url, test_data)
72-
73-
assert response.status_code == status.HTTP_400_BAD_REQUEST
74-
assert expected_message in response.data["detail"]
7571
assert response.status_code == status.HTTP_400_BAD_REQUEST
76-
assert expected_message in response.data["detail"]
72+
assert expected_message in response.content.decode()
7773

7874
@patch("ol_openedx_lti_utilities.views.LtiUser")
7975
def test_no_lti_user_found(self, mock_lti_user_model):
8076
"""Test when no LTI user exists for given email."""
8177
mock_qs = Mock()
82-
mock_qs.exists.return_value = False
78+
mock_qs.first.return_value = None
8379
mock_lti_user_model.objects.filter.return_value = mock_qs
84-
mock_lti_user_model.DoesNotExist = LtiUser.DoesNotExist
8580

86-
data = {"email": self.valid_email, "username": self.valid_username}
81+
payload = {"email": self.valid_email, "username": self.valid_username}
8782

88-
response = self.client.post(self.url, data)
83+
response = self.client.post(self.url, payload)
8984

9085
assert response.status_code == status.HTTP_404_NOT_FOUND
9186

@@ -105,97 +100,43 @@ def test_user_does_not_need_fixing(self, mock_lti_user_model):
105100
mock_qs.first.return_value = mock_lti_user
106101
mock_lti_user_model.objects.filter.return_value = mock_qs
107102

108-
data = {"email": self.valid_email, "username": self.valid_username}
103+
payload = {"email": self.valid_email, "username": self.valid_username}
109104

110-
response = self.client.post(self.url, data)
105+
response = self.client.post(self.url, payload)
111106

112107
assert response.status_code == status.HTTP_400_BAD_REQUEST
113-
assert "User does not need fixing" in response.data["detail"]
114-
115-
@patch("ol_openedx_lti_utilities.views.LtiUser")
116-
def test_lti_username_regex_matching(self, mock_lti_user_model):
117-
"""Test various username formats against LTI username regex."""
118-
test_cases = [
119-
(
120-
"A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0", # pragma: allowlist secret
121-
True,
122-
), # 40 chars, alphanumeric
123-
(
124-
"abcdefghijklmnopqrstuvwxyz1234567890ABCD", # pragma: allowlist secret
125-
True,
126-
), # pragma: allowlist secret
127-
(
128-
"A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9", # pragma: allowlist secret
129-
False,
130-
), # pragma: allowlist secret
131-
(
132-
"A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0X", # pragma: allowlist secret
133-
False,
134-
), # pragma: allowlist secret
135-
("normalusername", False), # Normal username
136-
(
137-
"A1B2-C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0", # pragma: allowlist secret
138-
False,
139-
), # pragma: allowlist secret
140-
("", False), # Empty string
141-
]
142-
143-
for username, should_match in test_cases:
144-
with self.subTest(username=username):
145-
mock_edx_user = Mock()
146-
mock_edx_user.username = username
147-
mock_edx_user.email = self.valid_email
148-
149-
mock_lti_user = Mock()
150-
mock_lti_user.edx_user = mock_edx_user
151-
152-
mock_qs = Mock()
153-
mock_qs.exists.return_value = True
154-
mock_qs.first.return_value = mock_lti_user
155-
mock_lti_user_model.objects.filter.return_value = mock_qs
156-
157-
data = {"email": self.valid_email, "username": self.valid_username}
158-
159-
with patch("ol_openedx_lti_utilities.views.User") as mock_user_model:
160-
mock_user = Mock()
161-
mock_user_model.objects.get.return_value = mock_user
162-
163-
response = self.client.post(self.url, data)
164-
165-
if should_match:
166-
assert response.status_code == status.HTTP_200_OK
167-
mock_user.save.assert_called_once()
168-
else:
169-
assert response.status_code == status.HTTP_400_BAD_REQUEST
170-
assert "User does not need fixing" in response.data["detail"]
108+
assert (
109+
"User with the given email does not appear to be an LTI-created user."
110+
in response.content.decode()
111+
)
171112

172113
def test_only_post_method_allowed(self):
173114
"""Test that only POST method is allowed."""
174-
data = {"email": self.valid_email, "username": self.valid_username}
115+
payload = {"email": self.valid_email, "username": self.valid_username}
175116

176117
# Test GET
177118
response = self.client.get(self.url)
178119
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
179120

180121
# Test PUT
181-
response = self.client.put(self.url, data)
122+
response = self.client.put(self.url, payload)
182123
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
183124

184125
# Test DELETE
185126
response = self.client.delete(self.url)
186127
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
187128

188129
# Test PATCH
189-
response = self.client.patch(self.url, data)
130+
response = self.client.patch(self.url, payload)
190131
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
191132

192133
@patch("ol_openedx_lti_utilities.views.log")
193134
@patch("ol_openedx_lti_utilities.views.LtiUser")
194135
def test_logging_on_missing_fields(self, mock_lti_user_model, mock_log): # noqa: ARG002
195136
"""Test that error is logged when required fields are missing."""
196-
data = {}
137+
payload = {}
197138

198-
response = self.client.post(self.url, data)
139+
response = self.client.post(self.url, payload)
199140

200141
assert response.status_code == status.HTTP_400_BAD_REQUEST
201142
mock_log.error.assert_called_once_with("email and username are required")
@@ -205,13 +146,12 @@ def test_logging_on_missing_fields(self, mock_lti_user_model, mock_log): # noqa
205146
def test_logging_on_no_user_found(self, mock_lti_user_model, mock_log):
206147
"""Test that error is logged when no LTI user is found."""
207148
mock_qs = Mock()
208-
mock_qs.exists.return_value = False
149+
mock_qs.first.return_value = None
209150
mock_lti_user_model.objects.filter.return_value = mock_qs
210-
mock_lti_user_model.DoesNotExist = LtiUser.DoesNotExist
211151

212-
data = {"email": self.valid_email, "username": self.valid_username}
152+
payload = {"email": self.valid_email, "username": self.valid_username}
213153

214-
response = self.client.post(self.url, data)
154+
response = self.client.post(self.url, payload)
215155

216156
assert response.status_code == status.HTTP_404_NOT_FOUND
217157
mock_log.error.assert_called_once_with(

0 commit comments

Comments
 (0)