diff --git a/src/grader-service/grader-service/main.py b/src/grader-service/grader-service/main.py index e7b954a6..3b3a8788 100644 --- a/src/grader-service/grader-service/main.py +++ b/src/grader-service/grader-service/main.py @@ -14,10 +14,14 @@ import shutil import sys +from secrets import token_hex + from flask import jsonify from pathlib import Path +from illumidesk.grades.senders import LTIGradesSenderControlFile + from . import create_app from .models import db from .models import GraderService @@ -32,14 +36,53 @@ app = create_app() +@app.route( + '/control-file/////', + methods=['POST'], +) +def register_control_file( + assignment_name: str, lis_outcome_service_url: str, lis_result_sourcedid: str, lms_user_id: str, course_id: str +): + """ + Creates a new grades control file + + Args: + assignment_name: string representation of the assignment name from the LMS (normalized) + lis_outcome_service_url: url endpoint that is used to send grades to the LMS with LTI 1.1 + lis_result_sourcedid: unique assignment or module identifier used with LTI 1.1 + lms_user_id: unique (opaque) user id + course_id: the course id within the lms + + Returns: + JSON: True/False on whether or not the grader service was successfully launched + + example: + ``` + { + success: "True" + } + ``` + """ + # launcher = GraderServiceLauncher(org_name=org_name, course_id=course_id) + # if not launcher.grader_deployment_exists(): + try: + control_file = LTIGradesSenderControlFile(f'/home/grader-{course_id}/{course_id}') + control_file.register_data(assignment_name, lis_outcome_service_url, lis_result_sourcedid, lms_user_id) + except Exception as e: + return jsonify(success=False, message=str(e)), 500 + + else: + return jsonify(success=True) + + @app.route('/services//', methods=['POST']) def launch(org_name: str, course_id: str): """ - Creates a new grader-notebook pod if not exists + Creates a new grader-notebook pod running as a JupyterHub unmanaged service if one does not exist. Args: org_name: the organization name - course_id: the grader's course id (label) + course_id: the grader's course id Returns: JSON: True/False on whether or not the grader service was successfully launched @@ -51,26 +94,25 @@ def launch(org_name: str, course_id: str): } ``` """ - launcher = GraderServiceLauncher(org_name=org_name, course_id=course_id) - if not launcher.grader_deployment_exists(): - try: - launcher.create_grader_deployment() - # Register the new service to local database - new_service = GraderService( - name=course_id, - course_id=course_id, - url=f'http://{launcher.grader_name}:8888', - api_token=launcher.grader_token, - ) - db.session.add(new_service) - db.session.commit() - # then do patch for jhub deployment - # with this the jhub pod will be restarted and get/load new services - launcher.update_jhub_deployment() - except Exception as e: - return jsonify(success=False, message=str(e)), 500 + # launcher = GraderServiceLauncher(org_name=org_name, course_id=course_id) + # if not launcher.grader_deployment_exists(): + try: + # launcher.create_grader_deployment() + # Register the new service to local database + new_service = GraderService( + name=course_id, + course_id=course_id, + url=f'http://{course_id}:8888', + api_token=token_hex(32), + ) + db.session.add(new_service) + db.session.commit() + # then do patch for jhub deployment + # with this the jhub pod will be restarted and get/load new services + # launcher.update_jhub_deployment() + except Exception as e: + return jsonify(success=False, message=str(e)), 500 - return jsonify(success=True) else: return jsonify(success=False, message=f'A grader service already exists for this course_id:{course_id}'), 409 diff --git a/src/grader-service/grader-service/models.py b/src/grader-service/grader-service/models.py index 10216f2c..97561a55 100644 --- a/src/grader-service/grader-service/models.py +++ b/src/grader-service/grader-service/models.py @@ -38,5 +38,7 @@ class GraderService(db.Model): admin = db.Column(db.Boolean, default=True) api_token = db.Column(db.String(150), nullable=True) + db.create_all() + def __repr__(self): return "".format(self.name, self.url) diff --git a/src/illumidesk/illumidesk/apis/setup_course_service.py b/src/illumidesk/illumidesk/apis/setup_course_service.py index 404b9128..3a077db4 100644 --- a/src/illumidesk/illumidesk/apis/setup_course_service.py +++ b/src/illumidesk/illumidesk/apis/setup_course_service.py @@ -43,7 +43,7 @@ async def create_assignment_source_dir(org_name: str, course_id: str, assignment return False -async def register_new_service(org_name: str, course_id: str) -> Bool: +async def register_new_service(org_name: str, course_id: str) -> bool: """ Helps to register (asynchronously) new course definition through the grader setup service Args: @@ -67,3 +67,38 @@ async def register_new_service(org_name: str, course_id: str) -> Bool: # the response can be found in e.response. logger.error(f'Grader-setup service returned an error: {e}') return False + + +async def register_control_file( + assignment_name: str, lis_outcome_service_url: str, lis_result_sourcedid: str, lms_user_id: str, course_id: str +) -> bool: + """ + Helps to register the control file to keep track of assignments and resource id's with the LMS to + send grades. + + Args: + assignment_name: string representation of the assignment name from the LMS (normalized) + lis_outcome_service_url: url endpoint that is used to send grades to the LMS with LTI 1.1 + lis_result_sourcedid: unique assignment or module identifier used with LTI 1.1 + lms_user_id: unique (opaque) user id + course_id: the course id within the lms + + Returns: True when a new the control file was created and saved, false otherwise + + """ + + client = AsyncHTTPClient() + try: + response = await client.fetch( + f'{SERVICE_BASE_URL}/control-file/{assignment_name}/{lis_outcome_service_url}/{lis_result_sourcedid}/{lms_user_id}/{course_id}', + headers=SERVICE_COMMON_HEADERS, + body='', + method='POST', + ) + logger.debug(f'Grader-setup service response: {response.body}') + return True + except HTTPError as e: + # HTTPError is raised for non-200 responses + # the response can be found in e.response. + logger.error(f'Grader-setup service returned an error: {e}') + return False diff --git a/src/illumidesk/illumidesk/authenticators/authenticator.py b/src/illumidesk/illumidesk/authenticators/authenticator.py index 5db19c2e..e1b5fc1d 100644 --- a/src/illumidesk/illumidesk/authenticators/authenticator.py +++ b/src/illumidesk/illumidesk/authenticators/authenticator.py @@ -20,6 +20,7 @@ from illumidesk.apis.jupyterhub_api import JupyterHubAPI from illumidesk.apis.nbgrader_service import NbGraderServiceHelper from illumidesk.apis.setup_course_service import register_new_service +from illumidesk.apis.setup_course_service import register_control_file from illumidesk.apis.setup_course_service import create_assignment_source_dir from illumidesk.authenticators.handlers import LTI11AuthenticateHandler @@ -30,8 +31,6 @@ from illumidesk.authenticators.validator import LTI11LaunchValidator from illumidesk.authenticators.validator import LTI13LaunchValidator -from illumidesk.grades.senders import LTIGradesSenderControlFile - logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -72,17 +71,39 @@ async def setup_course_hook( username = lti_utils.normalize_string(authentication['name']) lms_user_id = authentication['auth_state']['lms_user_id'] user_role = authentication['auth_state']['user_role'] + + # lti 1.1 specific items + lis_outcome_service_url = '' + lis_result_sourcedid = '' + assignment_name = '' + if 'lis_outcome_service_url' in authentication['auth_state']['lis_outcome_service_url']: + lis_outcome_service_url = authentication['auth_state']['lis_outcome_service_url'] + if 'lis_result_sourcedid' in authentication['auth_state']['lis_outcome_service_url']: + lis_result_sourcedid = authentication['auth_state']['lis_outcome_service_url'] + if 'assignment_name' in authentication['auth_state']['assignment_name']: + assignment_name = lti_utils.normalize_string(authentication['auth_state']['assignment_name']) + # register the user (it doesn't matter if it is a student or instructor) with her/his lms_user_id in nbgrader nb_service.add_user_to_nbgrader_gradebook(username, lms_user_id) # TODO: verify the logic to simplify groups creation and membership if user_is_a_student(user_role): # assign the user to 'nbgrader-' group in jupyterhub and gradebook await jupyterhub_api.add_student_to_jupyterhub_group(course_id, username) + # add or update the lti 1.1 grader control file + if lis_outcome_service_url and lis_result_sourcedid: + _ = await register_control_file( + lis_outcome_service_url=lis_outcome_service_url, + lis_result_sourcedid=lis_result_sourcedid, + assignment_name=assignment_name, + course_id=course_id, + lms_user_id=lms_user_id, + ) elif user_is_an_instructor(user_role): # assign the user in 'formgrade-' group await jupyterhub_api.add_instructor_to_jupyterhub_group(course_id, username) - # launch the new (?) grader-notebook as a service - setup_response = await register_new_service(org_name=ORG_NAME, course_id=course_id) + + # launch the new grader-notebook as a service + _ = await register_new_service(org_name=ORG_NAME, course_id=course_id) return authentication @@ -241,10 +262,7 @@ async def authenticate(self, handler: BaseHandler, data: Dict[str, str] = None) lis_outcome_service_url = args['lis_outcome_service_url'] if 'lis_result_sourcedid' in args and args['lis_result_sourcedid']: lis_result_sourcedid = args['lis_result_sourcedid'] - # only if both values exist we can register them to submit grades later - if lis_outcome_service_url and lis_result_sourcedid: - control_file = LTIGradesSenderControlFile(f'/home/grader-{course_id}/{course_id}') - control_file.register_data(assignment_name, lis_outcome_service_url, lms_user_id, lis_result_sourcedid) + # Assignment creation if assignment_name: nbgrader_service = NbGraderServiceHelper(course_id, True) @@ -259,9 +277,12 @@ async def authenticate(self, handler: BaseHandler, data: Dict[str, str] = None) return { 'name': username_normalized, 'auth_state': { + 'assignment_name': assignment_name, 'course_id': course_id, 'lms_user_id': lms_user_id, 'user_role': user_role, + 'lis_outcome_service_url': lis_outcome_service_url, + 'lis_result_sourcedid': lis_result_sourcedid, }, # noqa: E231 } @@ -379,7 +400,7 @@ async def authenticate( # noqa: C901 ] # if there is a resource link request then process additional steps if not validator.is_deep_link_launch(jwt_decoded): - await process_resource_link(self.log, course_id, jwt_decoded) + await process_resource_link_lti_13(self.log, course_id, jwt_decoded) lms_user_id = jwt_decoded['sub'] if 'sub' in jwt_decoded else username @@ -398,7 +419,7 @@ async def authenticate( # noqa: C901 } -async def process_resource_link( +async def process_resource_link_lti_13( logger: Any, course_id: str, jwt_body_decoded: Dict[str, Any], diff --git a/src/illumidesk/illumidesk/authenticators/constants.py b/src/illumidesk/illumidesk/authenticators/constants.py index fb38ca99..162d4102 100644 --- a/src/illumidesk/illumidesk/authenticators/constants.py +++ b/src/illumidesk/illumidesk/authenticators/constants.py @@ -342,6 +342,6 @@ } # the list of roles that recognize a user as a Student -DEFAULT_ROLE_NAMES_FOR_STUDENT = ['student', 'learner'] +DEFAULT_ROLE_NAMES_FOR_STUDENT = ['student', 'learner', 'Student', 'Learner'] # the list of roles that recognize a user as an Instructor DEFAULT_ROLE_NAMES_FOR_INSTRUCTOR = ['instructor', 'urn:lti:role:ims/lis/teachingassistant'] diff --git a/src/illumidesk/tests/illumidesk/authenticators/test_lti11_authenticator.py b/src/illumidesk/tests/illumidesk/authenticators/test_lti11_authenticator.py index 96600e01..02f46c59 100644 --- a/src/illumidesk/tests/illumidesk/authenticators/test_lti11_authenticator.py +++ b/src/illumidesk/tests/illumidesk/authenticators/test_lti11_authenticator.py @@ -47,9 +47,12 @@ async def test_authenticator_returns_auth_state_with_canvas_fields( expected = { 'name': 'student1-1091', 'auth_state': { + 'assignment_name': 'test-assignment', 'course_id': 'intro101', 'lms_user_id': '185d6c59731a553009ca9b59ca3a885100000', 'user_role': 'Instructor', + 'lis_outcome_service_url': 'http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=', + 'lis_result_sourcedid': 'feb-123-456-2929::28883', }, } assert result == expected @@ -78,9 +81,12 @@ async def test_authenticator_returns_auth_state_with_other_lms_vendor( expected = { 'name': 'student1', 'auth_state': { + 'assignment_name': 'test-assignment', 'course_id': 'intro101', 'lms_user_id': '185d6c59731a553009ca9b59ca3a885100000', 'user_role': 'Instructor', + 'lis_outcome_service_url': 'http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=', + 'lis_result_sourcedid': 'feb-123-456-2929::28883', }, } assert result == expected @@ -138,80 +144,6 @@ async def test_authenticator_uses_lti_utils_normalize_string( assert mock_normalize_string.called -@pytest.mark.asyncio -@patch('pathlib.Path.mkdir') -async def test_authenticator_uses_lti_grades_sender_control_file_with_student_role( - mock_mkdir, tmp_path, make_lti11_success_authentication_request_args, mock_nbhelper -): - """ - Is the LTIGradesSenderControlFile class register_data method called when setting the user_role with the - Student string? - """ - - def _change_flag(): - LTIGradesSenderControlFile.FILE_LOADED = True - - with patch.object(LTI11LaunchValidator, 'validate_launch_request', return_value=True): - with patch.object(LTIGradesSenderControlFile, 'register_data', return_value=None) as mock_register_data: - with patch.object( - LTIGradesSenderControlFile, '_loadFromFile', return_value=None - ) as mock_loadFromFileMethod: - mock_loadFromFileMethod.side_effect = _change_flag - authenticator = LTI11Authenticator() - handler = Mock(spec=RequestHandler) - request = HTTPServerRequest( - method='POST', - connection=Mock(), - ) - handler.request = request - handler.request.arguments = make_lti11_success_authentication_request_args( - lms_vendor='edx', role='Student' - ) - handler.request.get_argument = lambda x, strip=True: make_lti11_success_authentication_request_args( - 'Student' - )[x][0].decode() - - _ = await authenticator.authenticate(handler, None) - assert mock_register_data.called - - -@pytest.mark.asyncio -@patch('pathlib.Path.mkdir') -async def test_authenticator_uses_lti_grades_sender_control_file_with_learner_role( - mock_mkdir, tmp_path, make_lti11_success_authentication_request_args, mock_nbhelper -): - """ - Is the LTIGradesSenderControlFile class register_data method called when setting the user_role with the - Learner string? - """ - - def _change_flag(): - LTIGradesSenderControlFile.FILE_LOADED = True - - with patch.object(LTI11LaunchValidator, 'validate_launch_request', return_value=True): - with patch.object(LTIGradesSenderControlFile, 'register_data', return_value=None) as mock_register_data: - with patch.object( - LTIGradesSenderControlFile, '_loadFromFile', return_value=None - ) as mock_loadFromFileMethod: - mock_loadFromFileMethod.side_effect = _change_flag - authenticator = LTI11Authenticator() - handler = Mock(spec=RequestHandler) - request = HTTPServerRequest( - method='POST', - connection=Mock(), - ) - handler.request = request - handler.request.arguments = make_lti11_success_authentication_request_args( - lms_vendor='canvas', role='Learner' - ) - handler.request.get_argument = lambda x, strip=True: make_lti11_success_authentication_request_args( - 'Learner' - )[x][0].decode() - - _ = await authenticator.authenticate(handler, None) - assert mock_register_data.called - - @pytest.mark.asyncio @patch('pathlib.Path.mkdir') async def test_authenticator_uses_lti_grades_sender_control_file_with_instructor_role( @@ -246,7 +178,7 @@ def _change_flag(): )[x][0].decode() _ = await authenticator.authenticate(handler, None) - assert mock_register_data.called + assert mock_register_data.assert_not_called @pytest.mark.asyncio @@ -303,9 +235,12 @@ async def test_authenticator_returns_auth_state_with_missing_lis_outcome_service expected = { 'name': 'student1-1091', 'auth_state': { + 'assignment_name': 'test-assignment', 'course_id': 'intro101', 'lms_user_id': '185d6c59731a553009ca9b59ca3a885100000', 'user_role': 'Learner', + 'lis_outcome_service_url': '', + 'lis_result_sourcedid': 'feb-123-456-2929::28883', }, } assert result == expected @@ -336,9 +271,12 @@ async def test_authenticator_returns_auth_state_with_missing_lis_result_sourcedi expected = { 'name': 'student1-1091', 'auth_state': { + 'assignment_name': 'test-assignment', 'course_id': 'intro101', 'lms_user_id': '185d6c59731a553009ca9b59ca3a885100000', 'user_role': 'Learner', + 'lis_outcome_service_url': 'http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=', + 'lis_result_sourcedid': '', }, } assert result == expected @@ -369,9 +307,12 @@ async def test_authenticator_returns_auth_state_with_empty_lis_result_sourcedid( expected = { 'name': 'student1-1091', 'auth_state': { + 'assignment_name': 'test-assignment', 'course_id': 'intro101', 'lms_user_id': '185d6c59731a553009ca9b59ca3a885100000', 'user_role': 'Learner', + 'lis_outcome_service_url': 'http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=', + 'lis_result_sourcedid': '', }, } assert result == expected @@ -402,9 +343,12 @@ async def test_authenticator_returns_auth_state_with_empty_lis_outcome_service_u expected = { 'name': 'student1-1091', 'auth_state': { + 'assignment_name': 'test-assignment', 'course_id': 'intro101', 'lms_user_id': '185d6c59731a553009ca9b59ca3a885100000', 'user_role': 'Learner', + 'lis_outcome_service_url': '', + 'lis_result_sourcedid': 'feb-123-456-2929::28883', }, } assert result == expected @@ -439,9 +383,12 @@ async def test_authenticator_returns_correct_username_when_using_email_as_userna expected = { 'name': 'foo', 'auth_state': { + 'assignment_name': 'test-assignment', 'course_id': 'intro101', 'lms_user_id': '185d6c59731a553009ca9b59ca3a885100000', 'user_role': 'Instructor', + 'lis_outcome_service_url': 'http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=', + 'lis_result_sourcedid': 'feb-123-456-2929::28883', }, } assert result == expected @@ -474,9 +421,12 @@ async def test_authenticator_returns_correct_course_id_when_using_context_label_ expected = { 'name': 'foo', 'auth_state': { + 'assignment_name': 'test-assignment', 'course_id': 'intro101', 'lms_user_id': '185d6c59731a553009ca9b59ca3a885100000', 'user_role': 'Instructor', + 'lis_outcome_service_url': 'http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=', + 'lis_result_sourcedid': 'feb-123-456-2929::28883', }, } assert result == expected @@ -509,9 +459,12 @@ async def test_authenticator_returns_correct_course_id_when_using_context_title_ expected = { 'name': 'foo', 'auth_state': { + 'assignment_name': 'test-assignment', 'course_id': 'intro101', 'lms_user_id': '185d6c59731a553009ca9b59ca3a885100000', 'user_role': 'Instructor', + 'lis_outcome_service_url': 'http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=', + 'lis_result_sourcedid': 'feb-123-456-2929::28883', }, } assert result == expected @@ -546,9 +499,12 @@ async def test_authenticator_returns_correct_username_when_using_lis_person_name expected = { 'name': 'foo', 'auth_state': { + 'assignment_name': 'test-assignment', 'course_id': 'intro101', 'lms_user_id': '185d6c59731a553009ca9b59ca3a885100000', 'user_role': 'Instructor', + 'lis_outcome_service_url': 'http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=', + 'lis_result_sourcedid': 'feb-123-456-2929::28883', }, } assert result == expected @@ -583,9 +539,12 @@ async def test_authenticator_returns_correct_username_when_using_lis_person_name expected = { 'name': 'bar', 'auth_state': { + 'assignment_name': 'test-assignment', 'course_id': 'intro101', 'lms_user_id': '185d6c59731a553009ca9b59ca3a885100000', 'user_role': 'Instructor', + 'lis_outcome_service_url': 'http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=', + 'lis_result_sourcedid': 'feb-123-456-2929::28883', }, } assert result == expected @@ -620,9 +579,12 @@ async def test_authenticator_returns_correct_username_when_using_lis_person_name expected = { 'name': 'foobar', 'auth_state': { + 'assignment_name': 'test-assignment', 'course_id': 'intro101', 'lms_user_id': '185d6c59731a553009ca9b59ca3a885100000', 'user_role': 'Instructor', + 'lis_outcome_service_url': 'http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=', + 'lis_result_sourcedid': 'feb-123-456-2929::28883', }, } assert result == expected @@ -655,16 +617,30 @@ async def test_authenticator_returns_username_from_user_id_with_another_lms( ) result = await authenticator.authenticate(handler, None) expected = { - 'name': '185d6c59731a553009ca9b59c', + 'name': 'student1', 'auth_state': { + 'assignment_name': 'illumidesk', 'course_id': 'intro101', 'lms_user_id': '185d6c59731a553009ca9b59ca3a885100000', 'user_role': 'Instructor', + 'lis_outcome_service_url': 'http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=', + 'lis_result_sourcedid': 'feb-123-456-2929::28883', }, } assert result == expected +@pytest.mark.asyncio +@patch('illumidesk.authenticators.authenticator.LTI11LaunchValidator') +async def test_authenticator_returns_username_from_user_id_with_another_lms( + lti11_validator, make_lti11_success_authentication_request_args, gradesender_controlfile_mock, mock_nbhelper +): + """ + Ensure the lti 1.1 args for the control file are returned with the auth dict + """ + pass + + @pytest.mark.asyncio @patch('illumidesk.authenticators.authenticator.LTI11LaunchValidator') async def test_authenticator_returns_login_id_plus_user_id_as_username_with_canvas( @@ -696,9 +672,12 @@ async def test_authenticator_returns_login_id_plus_user_id_as_username_with_canv expected = { 'name': 'foobar-123123', 'auth_state': { + 'assignment_name': 'test-assignment', 'course_id': 'intro101', 'lms_user_id': '185d6c59731a553009ca9b59ca3a885100000', 'user_role': 'Instructor', + 'lis_outcome_service_url': 'http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=', + 'lis_result_sourcedid': 'feb-123-456-2929::28883', }, } assert result == expected diff --git a/src/illumidesk/tests/illumidesk/authenticators/test_setup_course_hook.py b/src/illumidesk/tests/illumidesk/authenticators/test_setup_course_hook.py index 4d1d5270..afb6feda 100644 --- a/src/illumidesk/tests/illumidesk/authenticators/test_setup_course_hook.py +++ b/src/illumidesk/tests/illumidesk/authenticators/test_setup_course_hook.py @@ -7,7 +7,6 @@ from tornado.web import RequestHandler from tornado.httpclient import AsyncHTTPClient -from unittest.mock import AsyncMock from unittest.mock import patch from illumidesk.apis.jupyterhub_api import JupyterHubAPI @@ -238,3 +237,78 @@ async def test_setup_course_hook_initialize_data_dict( assert expected_data['course_id'] == result['auth_state']['course_id'] assert expected_data['org'] == os.environ.get('ORGANIZATION_NAME') assert expected_data['domain'] == local_handler.request.host + + +@pytest.mark.asyncio() +async def test_setup_course_hook_sets_lti11_kvs( + setup_course_environ, + setup_course_hook_environ, + make_auth_state_dict, + make_http_response, + make_mock_request_handler, + mock_nbhelper, +): + """ + Ensure the setup course hook calls the register control file function if applicable. + """ + pass + + +@pytest.mark.asyncio() +async def test_setup_course_hook_does_not_call_add_instructor_to_jupyterhub_group_when_role_is_learner( + setup_course_environ, + setup_course_hook_environ, + make_auth_state_dict, + make_http_response, + make_mock_request_handler, + mock_nbhelper, +): + """ + Is the register_new_service function called when the user_role is learner or student? + """ + local_authenticator = Authenticator(post_auth_hook=setup_course_hook) + local_handler = make_mock_request_handler(RequestHandler, authenticator=local_authenticator) + local_authentication = make_auth_state_dict() + + with patch.object(NbGraderServiceHelper, 'add_user_to_nbgrader_gradebook', return_value=None): + with patch.object(JupyterHubAPI, 'add_student_to_jupyterhub_group', return_value=None): + with patch.object( + JupyterHubAPI, 'add_instructor_to_jupyterhub_group', return_value=None + ) as mock_add_instructor_to_jupyterhub_group: + with patch.object( + AsyncHTTPClient, + 'fetch', + return_value=make_http_response(handler=local_handler.request), + ): + await setup_course_hook(local_authenticator, local_handler, local_authentication) + assert not mock_add_instructor_to_jupyterhub_group.called + + +@pytest.mark.asyncio() +async def test_setup_course_hook_does_not_call_add_instructor_to_jupyterhub_group_when_role_is_learner( + setup_course_environ, + setup_course_hook_environ, + make_auth_state_dict, + make_http_response, + make_mock_request_handler, + mock_nbhelper, +): + """ + Is the register_new_service function called when the user_role is learner or student? + """ + local_authenticator = Authenticator(post_auth_hook=setup_course_hook) + local_handler = make_mock_request_handler(RequestHandler, authenticator=local_authenticator) + local_authentication = make_auth_state_dict() + + with patch.object(NbGraderServiceHelper, 'add_user_to_nbgrader_gradebook', return_value=None): + with patch.object(JupyterHubAPI, 'add_student_to_jupyterhub_group', return_value=None): + with patch.object( + JupyterHubAPI, 'add_instructor_to_jupyterhub_group', return_value=None + ) as mock_add_instructor_to_jupyterhub_group: + with patch.object( + AsyncHTTPClient, + 'fetch', + return_value=make_http_response(handler=local_handler.request), + ): + await setup_course_hook(local_authenticator, local_handler, local_authentication) + assert not mock_add_instructor_to_jupyterhub_group.called diff --git a/src/illumidesk/tests/illumidesk/conftest.py b/src/illumidesk/tests/illumidesk/conftest.py index bd2c46dd..9c3e1d32 100644 --- a/src/illumidesk/tests/illumidesk/conftest.py +++ b/src/illumidesk/tests/illumidesk/conftest.py @@ -151,7 +151,7 @@ def lti11_complete_launch_args(): 'lis_result_sourcedid': ['feb-123-456-2929::28883'.encode()], 'lti_version': ['LTI-1p0'.encode()], 'resource_link_id': ['888efe72d4bbbdf90619353bb8ab5965ccbe9b3f'.encode()], - 'resource_link_title': ['IllumiDesk'.encode()], + 'resource_link_title': ['Test-Assignment-Another-LMS'.encode()], 'roles': ['Learner'.encode()], 'tool_consumer_info_product_family_code': ['canvas'.encode()], 'tool_consumer_info_version': ['cloud'.encode()], @@ -483,14 +483,23 @@ def make_auth_state_dict() -> Dict[str, str]: """ def _make_auth_state_dict( - username: str = 'foo', course_id: str = 'intro101', lms_user_id: str = 'abc123', user_role: str = 'Learner' + username: str = 'foo', + assignment_name: str = 'myassignment', + course_id: str = 'intro101', + lms_user_id: str = 'abc123', + user_role: str = 'Learner', + lis_outcome_service_url: str = 'http://www.imsglobal.org/developers/LTI/test/v1p1/common/tool_consumer_outcome.php?b64=MTIzNDU6OjpzZWNyZXQ=', + lis_result_sourcedid: str = 'feb-123-456-2929::28883', ): return { 'name': username, 'auth_state': { + 'assignment_name': assignment_name, 'course_id': course_id, 'lms_user_id': lms_user_id, 'user_role': user_role, + 'lis_outcome_service_url': lis_outcome_service_url, + 'lis_result_sourcedid': lis_result_sourcedid, }, # noqa: E231 } @@ -579,7 +588,7 @@ def _make_lti11_success_authentication_request_args( 'lis_result_sourcedid': ['feb-123-456-2929::28883'.encode()], 'lti_version': ['LTI-1p0'.encode()], 'resource_link_id': ['888efe72d4bbbdf90619353bb8ab5965ccbe9b3f'.encode()], - 'resource_link_title': ['IllumiDesk'.encode()], + 'resource_link_title': ['Test-Assignment-Another-LMS'.encode()], 'roles': [role.encode()], 'tool_consumer_info_product_family_code': [lms_vendor.encode()], 'tool_consumer_info_version': ['cloud'.encode()],