test_masquerade.py 21 KB
Newer Older
ichuang committed
1
"""
2
Unit tests for masquerade.
ichuang committed
3
"""
Calen Pennington committed
4
import json
5
import pickle
6
from datetime import datetime
ichuang committed
7

8
import pytest
9
from django.conf import settings
10 11
from django.core.urlresolvers import reverse
from django.test import TestCase
12
from mock import patch
13
from pytz import UTC
14

15
from capa.tests.response_xml_factory import OptionResponseXMLFactory
16
from courseware.masquerade import CourseMasquerade, MasqueradingKeyValueStore, get_masquerading_user_group
17
from courseware.tests.factories import StaffFactory
18
from courseware.tests.helpers import LoginEnrollmentTestCase, masquerade_as_group_member
19
from courseware.tests.test_submitting_problems import ProblemSubmissionTestMixin
20
from nose.plugins.attrib import attr
21
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
22
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
23
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference, set_user_preference
24
from student.tests.factories import UserFactory
25
from xblock.runtime import DictKeyValueStore
26
from xmodule.modulestore.django import modulestore
27
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
28
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
29
from xmodule.partitions.partitions import Group, UserPartition
30 31


32
class MasqueradeTestCase(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
33
    """
34
    Base class for masquerade tests that sets up a test course and enrolls a user in the course.
35
    """
36 37 38
    @classmethod
    def setUpClass(cls):
        super(MasqueradeTestCase, cls).setUpClass()
39
        cls.course = CourseFactory.create(number='masquerade-test', metadata={'start': datetime.now(UTC)})
40 41
        cls.info_page = ItemFactory.create(
            category="course_info", parent_location=cls.course.location,
42 43
            data="OOGIE BLOOGIE", display_name="updates"
        )
44 45
        cls.chapter = ItemFactory.create(
            parent_location=cls.course.location,
46 47 48
            category="chapter",
            display_name="Test Section",
        )
49 50 51
        cls.sequential_display_name = "Test Masquerade Subsection"
        cls.sequential = ItemFactory.create(
            parent_location=cls.chapter.location,
52
            category="sequential",
53
            display_name=cls.sequential_display_name,
54
        )
55 56
        cls.vertical = ItemFactory.create(
            parent_location=cls.sequential.location,
57 58 59 60 61 62 63 64 65 66
            category="vertical",
            display_name="Test Unit",
        )
        problem_xml = OptionResponseXMLFactory().build_xml(
            question_text='The correct answer is Correct',
            num_inputs=2,
            weight=2,
            options=['Correct', 'Incorrect'],
            correct_option='Correct'
        )
67 68 69
        cls.problem_display_name = "TestMasqueradeProblem"
        cls.problem = ItemFactory.create(
            parent_location=cls.vertical.location,
70 71
            category='problem',
            data=problem_xml,
72
            display_name=cls.problem_display_name
73
        )
74 75 76 77

    def setUp(self):
        super(MasqueradeTestCase, self).setUp()

78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
        self.test_user = self.create_user()
        self.login(self.test_user.email, 'test')
        self.enroll(self.course, True)

    def get_courseware_page(self):
        """
        Returns the server response for the courseware page.
        """
        url = reverse(
            'courseware_section',
            kwargs={
                'course_id': unicode(self.course.id),
                'chapter': self.chapter.location.name,
                'section': self.sequential.location.name,
            }
        )
        return self.client.get(url)

96 97 98 99 100 101 102 103 104 105 106 107
    def get_course_info_page(self):
        """
        Returns the server response for course info page.
        """
        url = reverse(
            'info',
            kwargs={
                'course_id': unicode(self.course.id),
            }
        )
        return self.client.get(url)

108 109 110 111 112 113 114 115 116 117 118 119
    def get_progress_page(self):
        """
        Returns the server response for progress page.
        """
        url = reverse(
            'progress',
            kwargs={
                'course_id': unicode(self.course.id),
            }
        )
        return self.client.get(url)

120 121 122 123 124
    def verify_staff_debug_present(self, staff_debug_expected):
        """
        Verifies that the staff debug control visibility is as expected (for staff only).
        """
        content = self.get_courseware_page().content
125
        self.assertIn(self.sequential_display_name, content, "Subsection should be visible")
126
        self.assertEqual(staff_debug_expected, 'Staff Debug Info' in content)
ichuang committed
127

128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
    def get_problem(self):
        """
        Returns the JSON content for the problem in the course.
        """
        problem_url = reverse(
            'xblock_handler',
            kwargs={
                'course_id': unicode(self.course.id),
                'usage_id': unicode(self.problem.location),
                'handler': 'xmodule_handler',
                'suffix': 'problem_get'
            }
        )
        return self.client.get(problem_url)

    def verify_show_answer_present(self, show_answer_expected):
        """
        Verifies that "Show Answer" is only present when expected (for staff only).
        """
        problem_html = json.loads(self.get_problem().content)['html']
148
        self.assertIn(self.problem_display_name, problem_html)
149
        self.assertEqual(show_answer_expected, "Show Answer" in problem_html)
ichuang committed
150

151 152 153 154 155 156 157 158 159 160 161 162 163
    def ensure_masquerade_as_group_member(self, partition_id, group_id):
        """
        Installs a masquerade for the test_user and test course, to enable the
        user to masquerade as belonging to the specific partition/group combination.
        Also verifies that the call to install the masquerade was successful.

        Arguments:
            partition_id (int): the integer partition id, referring to partitions already
               configured in the course.
            group_id (int); the integer group id, within the specified partition.
        """
        self.assertEqual(200, masquerade_as_group_member(self.test_user, self.course, partition_id, group_id))

ichuang committed
164

165
@attr(shard=1)
166 167 168 169 170 171 172 173 174
class NormalStudentVisibilityTest(MasqueradeTestCase):
    """
    Verify the course displays as expected for a "normal" student (to ensure test setup is correct).
    """
    def create_user(self):
        """
        Creates a normal student user.
        """
        return UserFactory()
ichuang committed
175

176
    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
177
    @pytest.mark.django111_expected_failure
178 179 180 181 182 183 184 185 186 187 188 189
    def test_staff_debug_not_visible(self):
        """
        Tests that staff debug control is not present for a student.
        """
        self.verify_staff_debug_present(False)

    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
    def test_show_answer_not_visible(self):
        """
        Tests that "Show Answer" is not visible for a student.
        """
        self.verify_show_answer_present(False)
ichuang committed
190 191


192 193 194 195 196 197 198 199 200
class StaffMasqueradeTestCase(MasqueradeTestCase):
    """
    Base class for tests of the masquerade behavior for a staff member.
    """
    def create_user(self):
        """
        Creates a staff user.
        """
        return StaffFactory(course_key=self.course.id)
ichuang committed
201

202
    def update_masquerade(self, role, group_id=None, user_name=None):
203 204 205
        """
        Toggle masquerade state.
        """
206 207 208 209 210 211 212 213
        masquerade_url = reverse(
            'masquerade_update',
            kwargs={
                'course_key_string': unicode(self.course.id),
            }
        )
        response = self.client.post(
            masquerade_url,
214
            json.dumps({"role": role, "group_id": group_id, "user_name": user_name}),
215 216
            "application/json"
        )
217
        self.assertEqual(response.status_code, 200)
218 219 220
        return response


221
@attr(shard=1)
222 223 224 225 226
class TestStaffMasqueradeAsStudent(StaffMasqueradeTestCase):
    """
    Check for staff being able to masquerade as student.
    """
    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
227
    @pytest.mark.django111_expected_failure
228 229 230 231 232 233
    def test_staff_debug_with_masquerade(self):
        """
        Tests that staff debug control is not visible when masquerading as a student.
        """
        # Verify staff initially can see staff debug
        self.verify_staff_debug_present(True)
ichuang committed
234

235 236 237
        # Toggle masquerade to student
        self.update_masquerade(role='student')
        self.verify_staff_debug_present(False)
ichuang committed
238

239 240 241
        # Toggle masquerade back to staff
        self.update_masquerade(role='staff')
        self.verify_staff_debug_present(True)
242

243 244 245 246 247 248 249
    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
    def test_show_answer_for_staff(self):
        """
        Tests that "Show Answer" is not visible when masquerading as a student.
        """
        # Verify that staff initially can see "Show Answer".
        self.verify_show_answer_present(True)
250

251 252 253 254 255 256 257 258 259
        # Toggle masquerade to student
        self.update_masquerade(role='student')
        self.verify_show_answer_present(False)

        # Toggle masquerade back to staff
        self.update_masquerade(role='staff')
        self.verify_show_answer_present(True)


260
@attr(shard=1)
261 262 263 264 265 266 267 268 269 270 271 272
class TestStaffMasqueradeAsSpecificStudent(StaffMasqueradeTestCase, ProblemSubmissionTestMixin):
    """
    Check for staff being able to masquerade as a specific student.
    """
    def setUp(self):
        super(TestStaffMasqueradeAsSpecificStudent, self).setUp()
        self.student_user = self.create_user()
        self.login_student()
        self.enroll(self.course, True)

    def login_staff(self):
        """ Login as a staff user """
273
        self.logout()
274 275 276 277
        self.login(self.test_user.email, 'test')

    def login_student(self):
        """ Login as a student """
278
        self.logout()
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
        self.login(self.student_user.email, 'test')

    def submit_answer(self, response1, response2):
        """
        Submit an answer to the single problem in our test course.
        """
        return self.submit_question_answer(
            self.problem_display_name,
            {'2_1': response1, '2_2': response2}
        )

    def get_progress_detail(self):
        """
        Return the reported progress detail for the problem in our test course.

        The return value is a string like u'1/2'.
        """
296 297 298
        json_data = json.loads(self.look_at_question(self.problem_display_name).content)
        progress = '%s/%s' % (str(json_data['current_score']), str(json_data['total_possible']))
        return progress
299

300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
    def assertExpectedLanguageInPreference(self, user, expected_language_code):
        """
        This method is a custom assertion verifies that a given user has expected
        language code in the preference and in cookies.

        Arguments:
            user: User model instance
            expected_language_code: string indicating a language code
        """
        self.assertEqual(
            get_user_preference(user, LANGUAGE_KEY), expected_language_code
        )
        self.assertEqual(
            self.client.cookies[settings.LANGUAGE_COOKIE].value, expected_language_code
        )

316
    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
317
    @pytest.mark.django111_expected_failure
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
    def test_masquerade_as_specific_user_on_self_paced(self):
        """
        Test masquerading as a specific user for course info page when self paced configuration
        "enable_course_home_improvements" flag is set

        Login as a staff user and visit course info page.
        set masquerade to view same page as a specific student and revisit the course info page.
        """
        # Log in as staff, and check we can see the info page.
        self.login_staff()
        response = self.get_course_info_page()
        self.assertEqual(response.status_code, 200)
        content = response.content
        self.assertIn("OOGIE BLOOGIE", content)

        # Masquerade as the student,enable the self paced configuration, and check we can see the info page.
        SelfPacedConfiguration(enable_course_home_improvements=True).save()
        self.update_masquerade(role='student', user_name=self.student_user.username)
        response = self.get_course_info_page()
        self.assertEqual(response.status_code, 200)
        content = response.content
        self.assertIn("OOGIE BLOOGIE", content)

    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
342
    @pytest.mark.django111_expected_failure
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
    def test_masquerade_as_specific_student(self):
        """
        Test masquerading as a specific user.

        We answer the problem in our test course as the student and as staff user, and we use the
        progress as a proxy to determine who's state we currently see.
        """
        # Answer correctly as the student, and check progress.
        self.login_student()
        self.submit_answer('Correct', 'Correct')
        self.assertEqual(self.get_progress_detail(), u'2/2')

        # Log in as staff, and check the problem is unanswered.
        self.login_staff()
        self.assertEqual(self.get_progress_detail(), u'0/2')

        # Masquerade as the student, and check we can see the student state.
        self.update_masquerade(role='student', user_name=self.student_user.username)
        self.assertEqual(self.get_progress_detail(), u'2/2')

        # Temporarily override the student state.
        self.submit_answer('Correct', 'Incorrect')
        self.assertEqual(self.get_progress_detail(), u'1/2')

        # Reload the page and check we see the student state again.
        self.get_courseware_page()
        self.assertEqual(self.get_progress_detail(), u'2/2')

        # Become the staff user again, and check the problem is still unanswered.
        self.update_masquerade(role='staff')
        self.assertEqual(self.get_progress_detail(), u'0/2')

        # Verify the student state did not change.
        self.login_student()
        self.assertEqual(self.get_progress_detail(), u'2/2')

379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
    def test_masquerading_with_language_preference(self):
        """
        Tests that masquerading as a specific user for the course does not update preference language
        for the staff.

        Login as a staff user and set user's language preference to english and visit the courseware page.
        Set masquerade to view same page as a specific student having different language preference and
        revisit the courseware page.
        """
        english_language_code = 'en'
        set_user_preference(self.test_user, preference_key=LANGUAGE_KEY, preference_value=english_language_code)
        self.login_staff()

        # Reload the page and check we have expected language preference in system and in cookies.
        self.get_courseware_page()
        self.assertExpectedLanguageInPreference(self.test_user, english_language_code)

        # Set student language preference and set masquerade to view same page the student.
        set_user_preference(self.student_user, preference_key=LANGUAGE_KEY, preference_value='es-419')
        self.update_masquerade(role='student', user_name=self.student_user.username)

        # Reload the page and check we have expected language preference in system and in cookies.
        self.get_courseware_page()
        self.assertExpectedLanguageInPreference(self.test_user, english_language_code)

404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
    def test_masquerade_as_specific_student_course_info(self):
        """
        Test masquerading as a specific user for course info page.

        We login with login_staff and check course info page content if it's working and then we
        set masquerade to view same page as a specific student and test if it's working or not.
        """
        # Log in as staff, and check we can see the info page.
        self.login_staff()
        content = self.get_course_info_page().content
        self.assertIn("OOGIE BLOOGIE", content)

        # Masquerade as the student, and check we can see the info page.
        self.update_masquerade(role='student', user_name=self.student_user.username)
        content = self.get_course_info_page().content
        self.assertIn("OOGIE BLOOGIE", content)

422 423
    def test_masquerade_as_specific_student_progress(self):
        """
424
        Test masquerading as a specific user for progress page.
425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
        """
        # Give the student some correct answers, check their progress page
        self.login_student()
        self.submit_answer('Correct', 'Correct')
        student_progress = self.get_progress_page().content
        self.assertNotIn("1 of 2 possible points", student_progress)
        self.assertIn("2 of 2 possible points", student_progress)

        # Staff answers are slightly different
        self.login_staff()
        self.submit_answer('Incorrect', 'Correct')
        staff_progress = self.get_progress_page().content
        self.assertNotIn("2 of 2 possible points", staff_progress)
        self.assertIn("1 of 2 possible points", staff_progress)

        # Should now see the student's scores
        self.update_masquerade(role='student', user_name=self.student_user.username)
        masquerade_progress = self.get_progress_page().content
        self.assertNotIn("1 of 2 possible points", masquerade_progress)
        self.assertIn("2 of 2 possible points", masquerade_progress)

446

447
@attr(shard=1)
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
class TestGetMasqueradingGroupId(StaffMasqueradeTestCase):
    """
    Check for staff being able to masquerade as belonging to a group.
    """
    def setUp(self):
        super(TestGetMasqueradingGroupId, self).setUp()
        self.user_partition = UserPartition(
            0, 'Test User Partition', '',
            [Group(0, 'Group 1'), Group(1, 'Group 2')],
            scheme_id='cohort'
        )
        self.course.user_partitions.append(self.user_partition)
        modulestore().update_item(self.course, self.test_user.id)

    @patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
463
    def test_get_masquerade_group(self):
464
        """
465
        Tests that a staff member can masquerade as being in a group in a user partition
466
        """
467 468 469
        # Verify there is no masquerading group initially
        group = get_masquerading_user_group(self.course.id, self.test_user, self.user_partition)
        self.assertIsNone(group)
470 471

        # Install a masquerading group
472
        self.ensure_masquerade_as_group_member(0, 1)
473 474

        # Verify that the masquerading group is returned
475 476
        group = get_masquerading_user_group(self.course.id, self.test_user, self.user_partition)
        self.assertEqual(group.id, 1)
477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536


class ReadOnlyKeyValueStore(DictKeyValueStore):
    """
    A KeyValueStore that raises an exception on attempts to modify it.

    Used to make sure MasqueradingKeyValueStore does not try to modify the underlying KeyValueStore.
    """
    def set(self, key, value):
        assert False, "ReadOnlyKeyValueStore may not be modified."

    def delete(self, key):
        assert False, "ReadOnlyKeyValueStore may not be modified."

    def set_many(self, update_dict):  # pylint: disable=unused-argument
        assert False, "ReadOnlyKeyValueStore may not be modified."


class FakeSession(dict):
    """ Mock for Django session object. """
    modified = False  # We need dict semantics with a writable 'modified' property


class MasqueradingKeyValueStoreTest(TestCase):
    """
    Unit tests for the MasqueradingKeyValueStore class.
    """
    def setUp(self):
        super(MasqueradingKeyValueStoreTest, self).setUp()
        self.ro_kvs = ReadOnlyKeyValueStore({'a': 42, 'b': None, 'c': 'OpenCraft'})
        self.session = FakeSession()
        self.kvs = MasqueradingKeyValueStore(self.ro_kvs, self.session)

    def test_all(self):
        self.assertEqual(self.kvs.get('a'), 42)
        self.assertEqual(self.kvs.get('b'), None)
        self.assertEqual(self.kvs.get('c'), 'OpenCraft')
        with self.assertRaises(KeyError):
            self.kvs.get('d')

        self.assertTrue(self.kvs.has('a'))
        self.assertTrue(self.kvs.has('b'))
        self.assertTrue(self.kvs.has('c'))
        self.assertFalse(self.kvs.has('d'))

        self.kvs.set_many({'a': 'Norwegian Blue', 'd': 'Giraffe'})
        self.kvs.set('b', 7)

        self.assertEqual(self.kvs.get('a'), 'Norwegian Blue')
        self.assertEqual(self.kvs.get('b'), 7)
        self.assertEqual(self.kvs.get('c'), 'OpenCraft')
        self.assertEqual(self.kvs.get('d'), 'Giraffe')

        for key in 'abd':
            self.assertTrue(self.kvs.has(key))
            self.kvs.delete(key)
            with self.assertRaises(KeyError):
                self.kvs.get(key)

        self.assertEqual(self.kvs.get('c'), 'OpenCraft')
537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552


class CourseMasqueradeTest(TestCase):
    """
    Unit tests for the CourseMasquerade class.
    """
    def test_unpickling_sets_all_attributes(self):
        """
        Make sure that old CourseMasquerade objects receive missing attributes when unpickled from
        the session.
        """
        cmasq = CourseMasquerade(7)
        del cmasq.user_name
        pickled_cmasq = pickle.dumps(cmasq)
        unpickled_cmasq = pickle.loads(pickled_cmasq)
        self.assertEqual(unpickled_cmasq.user_name, None)