Skip to content

Commit f32eab4

Browse files
fix: review comments
1 parent 92eb28d commit f32eab4

2 files changed

Lines changed: 122 additions & 1 deletion

File tree

integrated_channels/moodle/client.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,107 @@
1717
from integrated_channels.utils import generate_formatted_log, stringify_and_store_api_record
1818

1919
MOODLE_ERROR_STATUS_MAP = {
20+
# Duplicate/Conflict errors (409)
2021
"shortnametaken": 409,
2122
"courseidnumbertaken": 409,
23+
"morethanonerecordinfetch": 409,
24+
"duplicateemail": 409,
25+
"duplicateidnumber": 409,
26+
"duplicateusername": 409,
27+
28+
# Not Found errors (404)
2229
"cannotfindcourse": 404,
2330
"cannotfinduser": 404,
31+
"invalidrecord": 404,
32+
"usernamenotfound": 404,
33+
"coursenotfound": 404,
34+
"usernotfound": 404,
35+
"invalidcourseid": 404,
36+
"invaliduserid": 404,
37+
"modulenotfound": 404,
38+
"activitynotfound": 404,
39+
40+
# Bad Request errors (400)
2441
"missingparam": 400,
42+
"codingerror": 400,
43+
"invalidparameter": 400,
44+
"invalidparametervalue": 400,
45+
"requiredparametermissing": 400,
46+
"invalidresponse": 400,
47+
"invaliddata": 400,
48+
49+
# Authentication errors (401)
50+
"invalidlogin": 401,
51+
"invalidtoken": 401,
52+
"sessiontimeout": 401,
53+
"requiresauthentication": 401,
54+
55+
# Authorization/Permission errors (403)
56+
"accessexception": 403,
57+
"errorcatcontextnotvalid": 403,
58+
"usernotfullysetup": 403,
59+
"nopermission": 403,
60+
"nopermissions": 403,
61+
"requirecapability": 403,
62+
"contextupdatefailed": 403,
63+
"notpermittedtoaccesscourse": 403,
64+
65+
# Server/Database errors (503)
66+
"dbconnectionfailed": 503,
67+
"dmlreadexception": 503,
68+
"dmlwriteexception": 503,
69+
"dmlexception": 503,
70+
"databaseerror": 503,
71+
72+
# Service/Method errors (400)
73+
"invalidservicename": 400,
74+
"invalidfunctionname": 400,
75+
"servicerequireslogin": 401,
76+
77+
# Enrollment errors (400)
78+
"cannotenrol": 400,
79+
"enrolnotpermitted": 403,
80+
"alreadyenrolled": 409,
81+
82+
# Grade/Completion errors (400)
83+
"invalidcompletion": 400,
84+
"invalidgrade": 400,
85+
"itemidnotfound": 404,
2586
}
2687

2788
LOGGER = logging.getLogger(__name__)
2889

2990

91+
def _effective_error_status_code(status_code, error_code=None):
92+
"""
93+
Map Moodle error codes to correct HTTP status codes.
94+
Moodle returns 200 response status code on failed transmission jobs which is the
95+
historical and default behavior of Moodle web services. This function corrects
96+
the status code based on the error_code in the response body.
97+
Args:
98+
status_code: The HTTP status code from the response
99+
error_code: The error code from Moodle's response body
100+
Returns:
101+
int: The effective status code (mapped from MOODLE_ERROR_STATUS_MAP,
102+
or 555 for unknown semantic errors, or original status_code)
103+
"""
104+
# First check if we have a mapped status for this error code
105+
if error_code and error_code in MOODLE_ERROR_STATUS_MAP:
106+
return MOODLE_ERROR_STATUS_MAP[error_code]
107+
108+
# If status code indicates error (>=400), keep it
109+
if status_code and status_code >= 400:
110+
return status_code
111+
112+
# If we get here, Moodle returned a 2xx/3xx status with an unknown error code
113+
# This is a semantic error - the HTTP status says success but there's an error
114+
if error_code:
115+
return 555 # Semantic error for unknown Moodle failure
116+
117+
# No error code and good status - this shouldn't happen in error path
118+
return status_code if status_code else 500
119+
120+
30121
class MoodleClientError(ClientError):
31122
"""
32123
Indicate a problem when interacting with Moodle.
@@ -102,14 +193,16 @@ def inner(self, *args, **kwargs):
102193
self.token = self._get_access_token() # pylint: disable=protected-access
103194
response = method(self, *args, **kwargs)
104195
elif error_code:
196+
status_code = _effective_error_status_code(response.status_code, error_code)
197+
105198
raise MoodleClientError(
106199
'Moodle API Client Task "{method}" failed with error code '
107200
'"{code}" and message: "{msg}" '.format(
108201
method=method.__name__,
109202
code=error_code,
110203
msg=body.get('message'),
111204
),
112-
response.status_code,
205+
status_code,
113206
moodle_error={
114207
'mapped_status': mapped_status,
115208
'error_code': error_code,

tests/test_integrated_channels/test_moodle/test_client.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,3 +501,31 @@ def test_missing_param_error_handling(self):
501501
'A required parameter is missing.',
502502
context.exception.message
503503
)
504+
505+
def test_moodle_errorcode_200_response_maps_to_correct_status(self):
506+
"""
507+
Test that when Moodle returns HTTP 200 with an errorcode,
508+
the client maps it to the correct HTTP status code using
509+
MOODLE_ERROR_STATUS_MAP.
510+
"""
511+
# pylint: disable=protected-access
512+
513+
client = MoodleAPIClient(self.enterprise_config)
514+
515+
mock_response = unittest.mock.Mock(spec=Response)
516+
mock_response.json.return_value = {
517+
"errorcode": "missingparam",
518+
"message": "A required parameter is missing."
519+
}
520+
mock_response.status_code = 200
521+
522+
client._post = unittest.mock.MagicMock(return_value=mock_response)
523+
524+
with self.assertRaises(MoodleClientError) as context:
525+
client._wrapped_post(SERIALIZED_DATA)
526+
527+
self.assertEqual(context.exception.status_code, 400)
528+
self.assertEqual(
529+
context.exception.moodle_error.get("error_code"),
530+
"missingparam"
531+
)

0 commit comments

Comments
 (0)