コード例 #1
0
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
コード例 #2
0
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
コード例 #3
0
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'
コード例 #4
0
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
コード例 #5
0
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'
コード例 #6
0
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
コード例 #7
0
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()
コード例 #8
0
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
コード例 #9
0
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
コード例 #10
0
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'
コード例 #11
0
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()
コード例 #12
0
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',
                )
コード例 #13
0
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']
コード例 #14
0
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)
コード例 #15
0
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
コード例 #16
0
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']
コード例 #17
0
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
コード例 #18
0
ファイル: conftest.py プロジェクト: artificialsoph/illumidesk
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()
コード例 #19
0
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)
コード例 #20
0
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']
コード例 #21
0
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']
コード例 #22
0
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'
コード例 #23
0
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
コード例 #24
0
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']
コード例 #25
0
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']
コード例 #26
0
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')
コード例 #27
0
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']
コード例 #28
0
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
コード例 #29
0
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()
コード例 #30
0
    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