async def test_setup_course_hook_calls_normalize_strings( auth_state_dict, setup_course_environ, setup_course_hook_environ): """ Does the setup_course_hook return normalized strings for the username and the course_id? """ local_authenticator = Authenticator(post_auth_hook=setup_course_hook) local_handler = mock_handler(RequestHandler, authenticator=local_authenticator) local_authentication = auth_state_dict with patch.object(LTIUtils, 'normalize_string', return_value='intro101') as mock_normalize_string: with patch.object(JupyterHubAPI, 'add_student_to_jupyterhub_group', return_value=None): with patch.object(JupyterHubAPI, 'add_user_to_nbgrader_gradebook', return_value=None): with patch.object(AsyncHTTPClient, 'fetch', return_value=factory_http_response( handler=local_handler.request)): _ = await setup_course_hook(local_authenticator, local_handler, local_authentication) assert mock_normalize_string.called
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): """ Is the jupyterhub_api add instructor to jupyterhub group function not called when the user role is the learner role? """ local_authenticator = Authenticator(post_auth_hook=setup_course_hook) local_handler = mock_handler(RequestHandler, authenticator=local_authenticator) local_authentication = factory_auth_state_dict() with patch.object(JupyterHubAPI, 'add_student_to_jupyterhub_group', return_value=None): with patch.object(JupyterHubAPI, 'add_user_to_nbgrader_gradebook', 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=factory_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
async def test_get_method_writes_lms_user_id_custom_field_within_each_course_navigation_placement( mock_write, lti_config_environ ): """ Does the get method write 'lms_user_id' field in custom_fields within each course_navigation placement setting? """ handler = mock_handler(RequestHandler) config_handler = LTI13ConfigHandler(handler.application, handler.request) # this method writes the output to internal buffer await config_handler.get() # call_args is a list # so we're only extracting the json arg json_arg = mock_write.call_args[0][0] extensions = json.loads(json_arg)['extensions'] course_navigation_placement = None for ext in extensions: # find the settings field in each extension to ensure a course_navigation placement was used if 'settings' in ext and 'placements' in ext['settings']: course_navigation_placement = [ placement for placement in ext['settings']['placements'] if placement['placement'] == 'course_navigation' ] assert course_navigation_placement placement_custom_fields = course_navigation_placement[0]['custom_fields'] assert placement_custom_fields assert placement_custom_fields['lms_user_id'] assert placement_custom_fields['lms_user_id'] == '$User.id'
async def test_setup_course_hook_initialize_data_dict( setup_course_environ, setup_course_hook_environ): """ Is the data dictionary correctly initialized when properly setting the org env-var and and consistent with the course id value in the auth state? """ local_authenticator = Authenticator(post_auth_hook=setup_course_hook) local_handler = mock_handler(RequestHandler, authenticator=local_authenticator) local_authentication = factory_auth_state_dict() expected_data = { 'org': 'test-org', 'course_id': 'intro101', 'domain': '127.0.0.1', } with patch.object(JupyterHubAPI, 'add_student_to_jupyterhub_group', return_value=None): with patch.object(JupyterHubAPI, 'add_user_to_nbgrader_gradebook', return_value=None): with patch.object(AsyncHTTPClient, 'fetch', return_value=factory_http_response( handler=local_handler.request)): result = await setup_course_hook(local_authenticator, local_handler, local_authentication) 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
async def test_lti_13_login_handler_sets_vars_for_redirect( monkeypatch, lti13_auth_params, lti13_auth_params_dict): """ Does the LTI13LoginHandler correctly set all variables needed for the redict method after receiving it from the validator? """ expected = lti13_auth_params_dict monkeypatch.setenv('LTI13_AUTHORIZE_URL', 'http://my.lms.platform/api/lti/authorize_redirect') local_handler = mock_handler(LTI13LoginHandler) local_utils = LTIUtils() with patch.object(LTIUtils, 'convert_request_to_dict', return_value=lti13_auth_params_dict): with patch.object(LTI13LaunchValidator, 'validate_launch_request', return_value=True): with patch.object(LTI13LoginHandler, 'redirect', return_value=None): assert expected['client_id'] == '125900000000000081' assert expected[ 'redirect_uri'] == 'https://acme.illumidesk.com/hub/oauth_callback' assert ( expected['lti_message_hint'] == 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ2ZXJpZmllciI6IjFlMjk2NjEyYjZmMjdjYmJkZTg5YmZjNGQ1ZmQ5ZDBhMzhkOTcwYzlhYzc0NDgwYzdlNTVkYzk3MTQyMzgwYjQxNGNiZjMwYzM5Nzk1Y2FmYTliOWYyYTgzNzJjNzg3MzAzNzAxZDgxMzQzZmRmMmIwZDk5ZTc3MWY5Y2JlYWM5IiwiY2FudmFzX2RvbWFpbiI6ImlsbHVtaWRlc2suaW5zdHJ1Y3R1cmUuY29tIiwiY29udGV4dF90eXBlIjoiQ291cnNlIiwiY29udGV4dF9pZCI6MTI1OTAwMDAwMDAwMDAwMTM2LCJleHAiOjE1OTE4MzMyNTh9.uYHinkiAT5H6EkZW9D7HJ1efoCmRpy3Id-gojZHlUaA' ) assert expected[ 'login_hint'] == '185d6c59731a553009ca9b59ca3a885104ecb4ad' assert ( expected['state'] == 'eyJzdGF0ZV9pZCI6ICI2ZjBlYzE1NjlhM2E0MDJkYWM2MTYyNjM2MWQwYzEyNSIsICJuZXh0X3VybCI6ICIvIn0=' ) assert expected['nonce'] == '38048502278109788461591832959'
async def test_get_method_calls_write_method(mock_write, lti_config_environ): """ Is the write method used in get method? """ handler = mock_handler(RequestHandler) config_handler = LTI13ConfigHandler(handler.application, handler.request) # this method writes the output to internal buffer await config_handler.get() assert mock_write.called
async def test_get_method_raises_an_error_without_lti13_private_key(): """ Is an environment error raised if the LTI13_PRIVATE_KEY env var is not set after calling the handler's method? """ handler = mock_handler(RequestHandler) config_handler = LTI13JWKSHandler(handler.application, handler.request) with pytest.raises(EnvironmentError): await config_handler.get()
async def test_lti_11_authenticate_handler_invokes_login_user_method(): """ Does the LTI11AuthenticateHandler call the login_user function? """ local_handler = mock_handler(LTI11AuthenticateHandler) with patch.object(LTI11AuthenticateHandler, 'redirect', return_value=None): with patch.object(LTI11AuthenticateHandler, 'login_user', return_value=None) as mock_login_user: await LTI11AuthenticateHandler(local_handler.application, local_handler.request).post() assert mock_login_user.called
async def test_authenticator_invokes_lti13validator_handler_get_argument(): """ Does the authenticator invoke the RequestHandler get_argument method? """ authenticator = LTI13Authenticator() request_handler = mock_handler(RequestHandler, authenticator=authenticator) with patch.object(request_handler, 'get_argument', return_value=dummy_lti13_id_token_complete.encode() ) as mock_get_argument: _ = await authenticator.authenticate(request_handler, None) assert mock_get_argument.called
async def test_get_method_writes_our_company_name_in_the_title_field(mock_write, lti_config_environ): """ Does the get method write 'Illumidesk' value as the title in the json? """ handler = mock_handler(RequestHandler) config_handler = LTI13ConfigHandler(handler.application, handler.request) # this method writes the output to internal buffer await config_handler.get() # call_args is a list # so we're only extracting the json arg json_arg = mock_write.call_args[0][0] title = json.loads(json_arg)['title'] assert title == 'IllumiDesk'
async def test_get_method_raises_permission_error_if_pem_file_is_protected( lti_config_environ): """ Is a permission error raised if the private key is protected after calling the handler's method? """ handler = mock_handler(RequestHandler) config_handler = LTI13JWKSHandler(handler.application, handler.request) # change pem permission key_path = environ.get('LTI13_PRIVATE_KEY') chmod(key_path, 0o060) with pytest.raises(PermissionError): await config_handler.get()
async def test_is_new_course_initiates_rolling_update( setup_course_environ, setup_course_hook_environ): """ If the course is a new setup does it initiate a rolling update? """ local_authenticator = Authenticator(post_auth_hook=setup_course_hook) local_handler = mock_handler(RequestHandler, authenticator=local_authenticator) local_authentication = factory_auth_state_dict() response_args = { 'handler': local_handler.request, 'body': { 'is_new_setup': True } } with patch.object( JupyterHubAPI, 'add_student_to_jupyterhub_group', return_value=None) as mock_add_student_to_jupyterhub_group: with patch.object(JupyterHubAPI, 'add_user_to_nbgrader_gradebook', return_value=None): with patch.object( AsyncHTTPClient, 'fetch', side_effect=[ factory_http_response(**response_args), factory_http_response(**response_args), None, ], # noqa: E231 ) as mock_client: await setup_course_hook(local_authenticator, local_handler, local_authentication) assert mock_client.called mock_client.assert_any_call( 'http://setup-course:8000/rolling-update', headers={'Content-Type': 'application/json'}, body='', method='POST', ) mock_client.assert_any_call( 'http://setup-course:8000', headers={'Content-Type': 'application/json'}, body= '{"org": "test-org", "course_id": "intro101", "domain": "127.0.0.1"}', method='POST', )
async def test_get_method_writes_lms_user_id_field_within_custom_fields(mock_write, lti_config_environ): """ Does the get method write 'lms_user_id' field within custom_fields and use the $User.id property? """ handler = mock_handler(RequestHandler) config_handler = LTI13ConfigHandler(handler.application, handler.request) # this method writes the output to internal buffer await config_handler.get() # call_args is a list # so we're only extracting the json arg json_arg = mock_write.call_args[0][0] custom_fields = json.loads(json_arg)['custom_fields'] assert 'lms_user_id' in custom_fields assert '$User.id' == custom_fields['lms_user_id']
async def test_get_calls_write_method_with_a_json(mock_write, lti_config_environ): """ Does the write base method is invoked with a string? """ handler = mock_handler(RequestHandler) config_handler = LTI13ConfigHandler(handler.application, handler.request) # this method writes the output to internal buffer await config_handler.get() # call_args is a list write_args = mock_write.call_args[0] # write_args == tuple json_arg = write_args[0] assert type(json_arg) == str assert json.loads(json_arg)
async def test_authenticator_invokes_lti13validator_validate_launch_request(): """ Does the authenticator invoke the LTI13Validator validate_launch_request method? """ authenticator = LTI13Authenticator() request_handler = mock_handler(RequestHandler, authenticator=authenticator) with patch.object(RequestHandler, 'get_argument', return_value=dummy_lti13_id_token_complete.encode()): with patch.object( LTI13LaunchValidator, 'validate_launch_request', return_value=True) as mock_verify_authentication_request: _ = await authenticator.authenticate(request_handler, None) assert mock_verify_authentication_request.called
async def test_get_method_writes_email_field_within_custom_fields(mock_write, lti_config_environ): """ Does the get method write 'email' field as a custom_fields? """ handler = mock_handler(RequestHandler) config_handler = LTI13ConfigHandler(handler.application, handler.request) # this method writes the output to internal buffer await config_handler.get() # call_args is a list # so we're only extracting the json arg json_arg = mock_write.call_args[0][0] custom_fields = json.loads(json_arg)['custom_fields'] assert 'email' in custom_fields assert '$Person.email.primary' == custom_fields['email']
async def test_authenticator_invokes_lti13validator_jwt_verify_and_decode(): """ Does the authenticator invoke the LTI13Validator jwt_verify_and_decode method? """ authenticator = LTI13Authenticator() request_handler = mock_handler(RequestHandler, authenticator=authenticator) with patch.object(RequestHandler, 'get_argument', return_value=dummy_lti13_id_token_complete.encode()): with patch.object(LTI13LaunchValidator, 'jwt_verify_and_decode', return_value=factory_lti13_resource_link_request() ) as mock_verify_and_decode: _ = await authenticator.authenticate(request_handler, None) assert mock_verify_and_decode.called
def http_async_httpclient_with_simple_response(request): """ Creates a patch of AsyncHttpClient.fetch method, useful when other tests are making http request """ local_handler = mock_handler(RequestHandler) test_request_body_param = request.param if hasattr(request, 'param') else { 'message': 'ok' } with patch.object( AsyncHTTPClient, 'fetch', return_value=factory_http_response(handler=local_handler.request, body=test_request_body_param), ): yield AsyncHTTPClient()
async def test_setup_course_hook_raises_environment_error_with_missing_org( monkeypatch, setup_course_hook_environ): """ Is an environment error raised when the organization name is missing when calling the setup_course_hook function? """ monkeypatch.setenv('ORGANIZATION_NAME', '') local_authenticator = Authenticator(post_auth_hook=setup_course_hook) local_handler = mock_handler(RequestHandler, authenticator=local_authenticator) local_authentication = factory_auth_state_dict() with pytest.raises(EnvironmentError): await local_authenticator.post_auth_hook(local_authenticator, local_handler, local_authentication)
async def test_authenticator_returns_course_id_in_auth_state_with_valid_resource_link_request( monkeypatch, auth_state_dict): """ Do we get a valid course_id when receiving a valid resource link request? """ authenticator = LTI13Authenticator() request_handler = mock_handler(RequestHandler, authenticator=authenticator) with patch.object(RequestHandler, 'get_argument', return_value=dummy_lti13_id_token_complete.encode()): with patch.object(LTI13LaunchValidator, 'validate_launch_request', return_value=True): result = await authenticator.authenticate(request_handler, None) monkeypatch.setitem(auth_state_dict, 'name', 'foo') assert result['name'] == auth_state_dict['name']
async def test_authenticator_returns_student_role_in_auth_state_with_student_role( monkeypatch, auth_state_dict): """ Do we set the student role in the auth_state when receiving a valid resource link request with the Student role? """ authenticator = LTI13Authenticator() request_handler = mock_handler(RequestHandler, authenticator=authenticator) monkeypatch.setitem(auth_state_dict['auth_state'], 'user_role', 'Learner') with patch.object(RequestHandler, 'get_argument', return_value=dummy_lti13_id_token_student_role.encode()): with patch.object(LTI13LaunchValidator, 'validate_launch_request', return_value=True): result = await authenticator.authenticate(request_handler, None) assert result['auth_state']['user_role'] == auth_state_dict[ 'auth_state']['user_role']
async def test_authenticator_returns_username_in_auth_state_with_with_given_name( monkeypatch, auth_state_dict): """ Do we get a valid username when only including the given name in the resource link request? """ authenticator = LTI13Authenticator() request_handler = mock_handler(RequestHandler, authenticator=authenticator) with patch.object( RequestHandler, 'get_argument', return_value=dummy_lti13_id_token_misssing_all_except_given_name. encode()): with patch.object(LTI13LaunchValidator, 'validate_launch_request', return_value=True): result = await authenticator.authenticate(request_handler, None) assert result['name'] == 'Foo Bar'
def send_grades_handler_lti13(): jhub_settings = {'authenticator_class': LTI13Authenticator} async def user_auth_state(): return [] def mock_user(): mock_user = Mock() attrs = { "get_auth_state.side_effect": user_auth_state, } mock_user.configure_mock(**attrs) return mock_user request_handler = mock_handler(RequestHandler, **jhub_settings) send_grades_handler = SendGradesHandler(request_handler.application, request_handler.request) setattr(send_grades_handler, '_jupyterhub_user', mock_user()) return send_grades_handler
async def test_authenticator_returns_vscode_workspace_image_with_vscode_workspace_type_in_auth_state( monkeypatch, auth_state_dict): """ Do we set the workspace image to the vscode image when setting the workspace type to vscode? """ authenticator = LTI13Authenticator() request_handler = mock_handler(RequestHandler, authenticator=authenticator) monkeypatch.setitem(auth_state_dict['auth_state'], 'workspace_type', 'vscode') with patch.object( RequestHandler, 'get_argument', return_value=dummy_lti13_id_token_vscode_workspace_type.encode()): with patch.object(LTI13LaunchValidator, 'validate_launch_request', return_value=True): result = await authenticator.authenticate(request_handler, None) assert result['auth_state']['workspace_type'] == auth_state_dict[ 'auth_state']['workspace_type']
async def test_authenticator_returns_learner_role_in_auth_state_with_empty_roles( monkeypatch, auth_state_dict): """ Do we set the learner role in the auth_state when receiving resource link request with empty roles? """ authenticator = LTI13Authenticator() request_handler = mock_handler(RequestHandler, authenticator=authenticator) monkeypatch.setitem(auth_state_dict['auth_state'], 'workspace_type', 'notebook') with patch.object(RequestHandler, 'get_argument', return_value=dummy_lti13_id_token_empty_roles.encode()): with patch.object(LTI13LaunchValidator, 'validate_launch_request', return_value=True): result = await authenticator.authenticate(request_handler, None) assert result['auth_state']['user_role'] == auth_state_dict[ 'auth_state']['user_role']
async def test_authenticator_returns_workspace_type_in_auth_state( monkeypatch, auth_state_dict): """ Do we get a valid lms_user_id in the auth_state when receiving a valid resource link request? """ authenticator = LTI13Authenticator() request_handler = mock_handler(RequestHandler, authenticator=authenticator) with patch.object(RequestHandler, 'get_argument', return_value=dummy_lti13_id_token_complete.encode()): with patch.object(LTI13LaunchValidator, 'validate_launch_request', return_value=True): result = await authenticator.authenticate(request_handler, None) monkeypatch.setitem(auth_state_dict, 'lms_user_id', '8171934b-f5e2-4f4e-bdbd-6d798615b93e') assert result['auth_state'].get( 'lms_user_id') == auth_state_dict.get('lms_user_id')
async def test_authenticator_returns_username_in_auth_state_with_person_sourcedid( monkeypatch, auth_state_dict): """ Do we get a valid username when only including lis person sourcedid resource link request? """ authenticator = LTI13Authenticator() request_handler = mock_handler(RequestHandler, authenticator=authenticator) with patch.object( RequestHandler, 'get_argument', return_value =dummy_lti13_id_token_misssing_all_except_person_sourcedid.encode( )): with patch.object(LTI13LaunchValidator, 'validate_launch_request', return_value=True): result = await authenticator.authenticate(request_handler, None) monkeypatch.setitem(auth_state_dict, 'name', 'abc123') assert result['name'] == auth_state_dict['name']
async def test_setup_course_hook_calls_add_instructor_to_jupyterhub_group_when_role_is_instructor( monkeypatch, setup_course_environ, setup_course_hook_environ): """ Is the jupyterhub_api add instructor to jupyterhub group function called when the user role is the instructor role? """ local_authenticator = Authenticator(post_auth_hook=setup_course_hook) local_handler = mock_handler(RequestHandler, authenticator=local_authenticator) local_authentication = factory_auth_state_dict(user_role='Instructor') 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=factory_http_response( handler=local_handler.request)): await setup_course_hook(local_authenticator, local_handler, local_authentication) assert mock_add_instructor_to_jupyterhub_group.called
async def test_lti_13_login_handler_empty_authorize_url_env_var_raises_environment_error( monkeypatch, lti13_login_params, lti13_login_params_dict): """ Does the LTI13LoginHandler raise a missing argument error if request body doesn't have any arguments? """ monkeypatch.setenv('LTI13_AUTHORIZE_URL', '') local_handler = mock_handler(LTI13LoginHandler) local_utils = LTIUtils() with patch.object(LTIUtils, 'convert_request_to_dict', return_value=lti13_login_params_dict ) as mock_convert_request_to_dict: with patch.object(LTI13LaunchValidator, 'validate_launch_request', return_value=True): with patch.object(LTI13LoginHandler, 'redirect', return_value=None): with pytest.raises(EnvironmentError): LTI13LoginHandler(local_handler.application, local_handler.request).post()
async def test_sender_calls__set_access_token_header_before_to_send_grades( self, lti_config_environ): sut = LTI13GradeSender('course-id', 'lab', { 'course_lineitems': 'canvas.docker.com/api/lti/courses/1/line_items' }) local_handler = mock_handler(RequestHandler) access_token_result = {'token_type': '', 'access_token': ''} line_item_result = { 'label': 'lab', 'id': 'line_item_url', 'scoreMaximum': 40 } with patch('illumidesk.grades.senders.get_lms_access_token', return_value=access_token_result) as mock_method: with patch.object( LTI13GradeSender, '_retrieve_grades_from_db', return_value=(lambda: 10, [{ 'score': 10, 'lms_user_id': 'id' }]), ): with patch.object( AsyncHTTPClient, 'fetch', side_effect=[ factory_http_response( handler=local_handler.request, body=[line_item_result]), factory_http_response( handler=local_handler.request, body=line_item_result), factory_http_response( handler=local_handler.request, body=[]), ], ): await sut.send_grades() assert mock_method.called