lti.py 13.9 KB
Newer Older
1
# pylint: disable=missing-docstring
2
import datetime
3
import os
4
import pytz
5
from django.conf import settings
6
from mock import patch
7 8
from pytz import UTC
from splinter.exceptions import ElementDoesNotExist
9 10
from selenium.common.exceptions import NoAlertPresentException
from nose.tools import assert_true, assert_equal, assert_in, assert_is_none
11 12
from lettuce import world, step

13 14 15
from courseware.tests.factories import InstructorFactory, BetaTesterFactory
from courseware.access import has_access
from student.tests.factories import UserFactory
16

17
from common import visit_scenario_item
18

19 20
TEST_COURSE_NAME = "test_course_a"

21

22
@step('I view the LTI and error is shown$')
23 24
def lti_is_not_rendered(_step):
    # error is shown
polesye committed
25
    assert world.is_css_present('.error_message', wait_time=0)
26

27
    # iframe is not presented
polesye committed
28
    assert not world.is_css_present('iframe', wait_time=0)
29

30
    # link is not presented
polesye committed
31
    assert not world.is_css_present('.link_lti_new_window', wait_time=0)
32 33


34
def check_lti_iframe_content(text):
35
    # inside iframe test content is presented
polesye committed
36
    location = world.scenario_dict['LTI'].location.html_id()
polesye committed
37
    iframe_name = 'ltiFrame-' + location
polesye committed
38 39
    with world.browser.get_iframe(iframe_name) as iframe:
        # iframe does not contain functions from terrain/ui_helpers.py
polesye committed
40
        assert iframe.is_element_present_by_css('.result', wait_time=0)
polesye committed
41 42 43 44 45 46
        assert (text == world.retry_on_exception(
            lambda: iframe.find_by_css('.result')[0].text,
            max_attempts=5
        ))


47 48 49
@step('I view the LTI and it is rendered in (.*)$')
def lti_is_rendered(_step, rendered_in):
    if rendered_in.strip() == 'iframe':
50
        world.wait_for_present('iframe')
polesye committed
51 52 53
        assert world.is_css_present('iframe', wait_time=2)
        assert not world.is_css_present('.link_lti_new_window', wait_time=0)
        assert not world.is_css_present('.error_message', wait_time=0)
54

55 56 57
        # iframe is visible
        assert world.css_visible('iframe')
        check_lti_iframe_content("This is LTI tool. Success.")
58

59
    elif rendered_in.strip() == 'new page':
polesye committed
60 61 62
        assert not world.is_css_present('iframe', wait_time=2)
        assert world.is_css_present('.link_lti_new_window', wait_time=0)
        assert not world.is_css_present('.error_message', wait_time=0)
63
        click_and_check_lti_popup()
64
    else:  # incorrect rendered_in parameter
65
        assert False
66 67


68 69 70 71 72 73 74 75 76 77 78
@step('I view the permission alert$')
def view_lti_permission_alert(_step):
    assert not world.is_css_present('iframe', wait_time=2)
    assert world.is_css_present('.link_lti_new_window', wait_time=0)
    assert not world.is_css_present('.error_message', wait_time=0)
    world.css_find('.link_lti_new_window').first.click()
    alert = world.browser.get_alert()
    assert alert is not None
    assert len(world.browser.windows) == 1


79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
def check_no_alert():
    """
    Make sure the alert has gone away.

    Note that the splinter documentation indicates that
    get_alert should return None if no alert is present,
    however that is not the case. Instead a
    NoAlertPresentException is raised.
    """
    try:
        assert_is_none(world.browser.get_alert())
    except NoAlertPresentException:
        pass


94 95 96
@step('I accept the permission alert and view the LTI$')
def accept_lti_permission_alert(_step):
    parent_window = world.browser.current_window  # Save the parent window
97 98

    # To start with you should only have one window/tab
99 100 101
    assert len(world.browser.windows) == 1
    alert = world.browser.get_alert()
    alert.accept()
102 103 104 105 106 107 108 109 110 111
    check_no_alert()

    # Give it a few seconds for the LTI window to appear
    world.wait_for(
        lambda _: len(world.browser.windows) == 2,
        timeout=5,
        timeout_msg="Timed out waiting for the LTI window to appear."
    )

    # Verify the LTI window
112 113 114 115 116 117 118
    check_lti_popup(parent_window)


@step('I reject the permission alert and do not view the LTI$')
def reject_lti_permission_alert(_step):
    alert = world.browser.get_alert()
    alert.dismiss()
119
    check_no_alert()
120 121 122
    assert len(world.browser.windows) == 1


123 124
@step('I view the LTI but incorrect_signature warning is rendered$')
def incorrect_lti_is_rendered(_step):
polesye committed
125 126 127
    assert world.is_css_present('iframe', wait_time=2)
    assert not world.is_css_present('.link_lti_new_window', wait_time=0)
    assert not world.is_css_present('.error_message', wait_time=0)
128

129
    # inside iframe test content is presented
130
    check_lti_iframe_content("Wrong LTI signature")
131 132


133 134
@step('the course has correct LTI credentials with registered (.*)$')
def set_correct_lti_passport(_step, user='Instructor'):
135
    coursenum = TEST_COURSE_NAME
136
    metadata = {
137
        'lti_passports': ["correct_lti_id:test_client_key:test_client_secret"]
138
    }
139

140
    i_am_registered_for_the_course(coursenum, metadata, user)
141 142 143 144


@step('the course has incorrect LTI credentials$')
def set_incorrect_lti_passport(_step):
145
    coursenum = TEST_COURSE_NAME
146
    metadata = {
147
        'lti_passports': ["test_lti_id:test_client_key:incorrect_lti_secret_key"]
148
    }
149

150 151
    i_am_registered_for_the_course(coursenum, metadata)

152

153
@step('the course has an LTI component with (.*) fields(?:\:)?$')  # , new_page is(.*), graded is(.*)
154
def add_correct_lti_to_course(_step, fields):
155
    category = 'lti'
156 157
    metadata = {
        'lti_id': 'correct_lti_id',
158
        'launch_url': 'http://127.0.0.1:{}/correct_lti_endpoint'.format(settings.LTI_PORT),
159
    }
160

161
    if fields.strip() == 'incorrect_lti_id':  # incorrect fields
162 163 164
        metadata.update({
            'lti_id': 'incorrect_lti_id'
        })
165 166 167
    elif fields.strip() == 'correct':  # correct fields
        pass
    elif fields.strip() == 'no_launch_url':
168 169 170
        metadata.update({
            'launch_url': u''
        })
171 172 173
    else:  # incorrect parameter
        assert False

174 175
    if _step.hashes:
        metadata.update(_step.hashes[0])
176

polesye committed
177
    world.scenario_dict['LTI'] = world.ItemFactory.create(
178
        parent_location=world.scenario_dict['SECTION'].location,
179 180
        category=category,
        display_name='LTI',
181
        metadata=metadata,
182
    )
183 184 185 186 187 188

    setattr(world.scenario_dict['LTI'], 'TEST_BASE_PATH', '{host}:{port}'.format(
        host=world.browser.host,
        port=world.browser.port,
    ))

189
    visit_scenario_item('LTI')
190 191


192
def create_course_for_lti(course, metadata):
193 194 195 196 197
    # First clear the modulestore so we don't try to recreate
    # the same course twice
    # This also ensures that the necessary templates are loaded
    world.clear_courses()

198 199 200 201 202 203 204 205 206 207 208 209 210
    weight = 0.1
    grading_policy = {
        "GRADER": [
            {
                "type": "Homework",
                "min_count": 1,
                "drop_count": 0,
                "short_label": "HW",
                "weight": weight
            },
        ]
    }

211 212 213 214 215 216 217
    # Create the course
    # We always use the same org and display name,
    # but vary the course identifier (e.g. 600x or 191x)
    world.scenario_dict['COURSE'] = world.CourseFactory.create(
        org='edx',
        number=course,
        display_name='Test Course',
218
        metadata=metadata,
219
        grading_policy=grading_policy,
220 221 222
    )

    # Add a section to the course to contain problems
223
    world.scenario_dict['CHAPTER'] = world.ItemFactory.create(
224
        parent_location=world.scenario_dict['COURSE'].location,
225 226
        category='chapter',
        display_name='Test Chapter',
227
    )
228 229
    world.scenario_dict['SECTION'] = world.ItemFactory.create(
        parent_location=world.scenario_dict['CHAPTER'].location,
230
        category='sequential',
231 232
        display_name='Test Section',
        metadata={'graded': True, 'format': 'Homework'})
233 234


235 236 237 238 239 240 241 242 243 244 245 246
@patch.dict('courseware.access.settings.FEATURES', {'DISABLE_START_DATES': False})
def i_am_registered_for_the_course(coursenum, metadata, user='Instructor'):
    # Create user
    if user == 'BetaTester':
        # Create the course
        now = datetime.datetime.now(pytz.UTC)
        tomorrow = now + datetime.timedelta(days=5)
        metadata.update({'days_early_for_beta': 5, 'start': tomorrow})
        create_course_for_lti(coursenum, metadata)
        course_descriptor = world.scenario_dict['COURSE']

        # create beta tester
247
        user = BetaTesterFactory(course_key=course_descriptor.id)
248
        normal_student = UserFactory()
249
        instructor = InstructorFactory(course_key=course_descriptor.id)
250

251 252 253
        assert not has_access(normal_student, 'load', course_descriptor)
        assert has_access(user, 'load', course_descriptor)
        assert has_access(instructor, 'load', course_descriptor)
254
    else:
255
        metadata.update({'start': datetime.datetime(1970, 1, 1, tzinfo=UTC)})
256 257
        create_course_for_lti(coursenum, metadata)
        course_descriptor = world.scenario_dict['COURSE']
258
        user = InstructorFactory(course_key=course_descriptor.id)
259

260
    # Enroll the user in the course and log them in
261 262
    if has_access(user, 'load', course_descriptor):
        world.enroll_user(user, course_descriptor.id)
263

264
    world.log_in(username=user.username, password='test')
265 266


267
def check_lti_popup(parent_window):
268 269 270 271 272 273 274
    # You should now have 2 browser windows open, the original courseware and the LTI
    windows = world.browser.windows
    assert_equal(len(windows), 2)

    # For verification, iterate through the window titles and make sure that
    # both are there.
    tabs = []
275 276
    expected_tabs = [u'LTI | Test Section | {0} Courseware | edX'.format(TEST_COURSE_NAME), u'TEST TITLE']

277 278 279
    for window in windows:
        world.browser.switch_to_window(window)
        tabs.append(world.browser.title)
280
    assert_equal(tabs, expected_tabs)   # pylint: disable=no-value-for-parameter
281 282 283 284 285 286 287 288 289

    # Now verify the contents of the LTI window (which is the 2nd window/tab)
    # Note: The LTI opens in a new browser window, but Selenium sticks with the
    # current window until you explicitly switch to the context of the new one.
    world.browser.switch_to_window(windows[1])
    url = world.browser.url
    basename = os.path.basename(url)
    pathname = os.path.splitext(basename)[0]
    assert_equal(pathname, u'correct_lti_endpoint')
290 291

    result = world.css_find('.result').first.text
292
    assert_equal(result, u'This is LTI tool. Success.')
293

294 295
    world.browser.driver.close()  # Close the pop-up window
    world.browser.switch_to_window(parent_window)  # Switch to the main window again
296 297


298 299 300 301 302 303
def click_and_check_lti_popup():
    parent_window = world.browser.current_window  # Save the parent window
    world.css_find('.link_lti_new_window').first.click()
    check_lti_popup(parent_window)


304 305 306 307 308 309 310 311 312 313
@step('visit the LTI component')
def visit_lti_component(_step):
    visit_scenario_item('LTI')


@step('I see LTI component (.*) with text "([^"]*)"$')
def see_elem_text(_step, elem, text):
    selector_map = {
        'progress': '.problem-progress',
        'feedback': '.problem-feedback',
314 315 316
        'module title': '.problem-header',
        'button': '.link_lti_new_window',
        'description': '.lti-description'
317 318 319 320 321
    }
    assert_in(elem, selector_map)
    assert_true(world.css_has_text(selector_map[elem], text))


322 323 324 325 326 327 328
@step('I see text "([^"]*)"$')
def check_progress(_step, text):
    assert world.browser.is_text_present(text)


@step('I see graph with total progress "([^"]*)"$')
def see_graph(_step, progress):
329 330 331
    selector = 'grade-detail-graph'
    xpath = '//div[@id="{parent}"]//div[text()="{progress}"]'.format(
        parent=selector,
332
        progress=progress,
polesye committed
333
    )
334
    node = world.browser.find_by_xpath(xpath)
335 336 337 338 339 340

    assert node


@step('I see in the gradebook table that "([^"]*)" is "([^"]*)"$')
def see_value_in_the_gradebook(_step, label, text):
341
    table_selector = '.grade-table'
342
    index = 0
343
    table_headers = world.css_find('{0} thead th'.format(table_selector))
344 345 346 347

    for i, element in enumerate(table_headers):
        if element.text.strip() == label:
            index = i
348
            break
349

350
    assert_true(world.css_has_text('{0} tbody td'.format(table_selector), text, index=index))
351 352


353 354 355 356 357 358 359
@step('I submit answer to LTI (.*) question$')
def click_grade(_step, version):
    version_map = {
        '1': {'selector': 'submit-button', 'expected_text': 'LTI consumer (edX) responded with XML content'},
        '2': {'selector': 'submit-lti2-button', 'expected_text': 'LTI consumer (edX) responded with HTTP 200'},
    }
    assert_in(version, version_map)
360
    location = world.scenario_dict['LTI'].location.html_id()
polesye committed
361
    iframe_name = 'ltiFrame-' + location
362
    with world.browser.get_iframe(iframe_name) as iframe:
363 364 365 366 367 368 369 370 371 372 373 374 375
        iframe.find_by_name(version_map[version]['selector']).first.click()
        assert iframe.is_text_present(version_map[version]['expected_text'])


@step('LTI provider deletes my grade and feedback$')
def click_delete_button(_step):
    with world.browser.get_iframe(get_lti_frame_name()) as iframe:
        iframe.find_by_name('submit-lti2-delete-button').first.click()


def get_lti_frame_name():
    location = world.scenario_dict['LTI'].location.html_id()
    return 'ltiFrame-' + location
376

377 378 379

@step('I see in iframe that LTI role is (.*)$')
def check_role(_step, role):
380
    world.wait_for_present('iframe')
381 382 383 384 385 386 387 388 389 390 391 392 393 394
    location = world.scenario_dict['LTI'].location.html_id()
    iframe_name = 'ltiFrame-' + location
    with world.browser.get_iframe(iframe_name) as iframe:
        expected_role = 'Role: ' + role
        role = world.retry_on_exception(
            lambda: iframe.find_by_tag('h5').first.value,
            max_attempts=5,
            ignored_exceptions=ElementDoesNotExist
        )
        assert_equal(expected_role, role)


@step('I switch to (.*)$')
def switch_view(_step, view):
395 396 397
    staff_status = world.css_find('#action-preview-select').first.value
    if staff_status != view:
        world.browser.select("select", view)
398
        world.wait_for_ajax_complete()
399
        assert_equal(world.css_find('#action-preview-select').first.value, view)
400

401 402 403 404 405 406 407 408 409 410 411

@step("in the LTI component I do not see (.*)$")
def check_lti_component_no_elem(_step, text):
    selector_map = {
        'a launch button': '.link_lti_new_window',
        'an provider iframe': '.ltiLaunchFrame',
        'feedback': '.problem-feedback',
        'progress': '.problem-progress',
    }
    assert_in(text, selector_map)
    assert_true(world.is_css_not_present(selector_map[text]))