Skip to content
base_test_mixins.py 110 KiB
Newer Older

        return dict_to_struct(flow_desc_dict)

    def get_hacked_flow_desc_with_access_rule_tags(self, rule_tags):
        assert isinstance(rule_tags, list)
Andreas Klöckner's avatar
Andreas Klöckner committed
        from relate.utils import dict_to_struct, struct_to_dict
        hacked_flow_desc_dict = self.get_hacked_flow_desc(as_dict=True)
        rules = hacked_flow_desc_dict["rules"]
        rules_dict = struct_to_dict(rules)
        rules_dict["tags"] = rule_tags
        rules = dict_to_struct(rules_dict)
        hacked_flow_desc_dict["rules"] = rules
        hacked_flow_desc = dict_to_struct(hacked_flow_desc_dict)
        assert hacked_flow_desc.rules.tags == rule_tags
        return hacked_flow_desc

# }}}


# {{{ TwoCourseTestMixin

class TwoCourseTestMixin(CoursesTestMixinBase):
Dong Zhuang's avatar
Dong Zhuang committed
    courses_setup_list = TWO_COURSE_SETUP_LIST
    def setUpTestData(cls):
        super().setUpTestData()
        assert len(cls.course_qset) == 2, (
            "'courses_setup_list' should contain two courses")
        cls.course1 = cls.course_qset.first()
        cls.course1_instructor_participation = Participation.objects.filter(
            course=cls.course1,
            roles__identifier="instructor",
            status=participation_status.active
        ).first()
        assert cls.course1_instructor_participation

        cls.course1_student_participation = Participation.objects.filter(
            course=cls.course1,
            roles__identifier="student",
            status=participation_status.active
        ).first()
        assert cls.course1_student_participation

        cls.course1_ta_participation = Participation.objects.filter(
            course=cls.course1,
            roles__identifier="ta",
            status=participation_status.active
        ).first()
        assert cls.course1_ta_participation
        cls.course1_page_url = cls.get_course_page_url(cls.course1.identifier)

        cls.course2 = cls.course_qset.last()
        cls.course2_instructor_participation = Participation.objects.filter(
            course=cls.course2,
            roles__identifier="instructor",
            status=participation_status.active
        ).first()
        assert cls.course2_instructor_participation

        cls.course2_student_participation = Participation.objects.filter(
            course=cls.course2,
            roles__identifier="student",
            status=participation_status.active
        ).first()
        assert cls.course2_student_participation

        cls.course2_ta_participation = Participation.objects.filter(
            course=cls.course2,
            roles__identifier="ta",
            status=participation_status.active
        ).first()
        assert cls.course2_ta_participation
        cls.course2_page_url = cls.get_course_page_url(cls.course2.identifier)

    def setUp(self):
        # reload objects created during setUpTestData in case they were modified in
        # tests. Ref: https://goo.gl/AuzJRC#django.test.TestCase.setUpTestData
        self.course1.refresh_from_db()
        self.course1_instructor_participation.refresh_from_db()
        self.course1_student_participation.refresh_from_db()
        self.course1_ta_participation.refresh_from_db()

        self.course2.refresh_from_db()
        self.course2_instructor_participation.refresh_from_db()
        self.course2_student_participation.refresh_from_db()
        self.course2_ta_participation.refresh_from_db()

# }}}


# {{{ SingleCoursePageTestMixin

class SingleCoursePageTestMixin(SingleCourseTestMixin):
    # This serves as cache
    _default_session_id = None

Dong Zhuang's avatar
Dong Zhuang committed
    flow_id = QUIZ_FLOW_ID

    @classmethod
    def update_default_flow_session_id(cls, course_identifier):
        cls._default_session_id = cls.default_flow_params["flow_session_id"]

    @classmethod
    def get_default_flow_session_id(cls, course_identifier):
        if cls._default_session_id is not None:
            return cls._default_session_id
        cls._default_session_id = cls.get_latest_session_id(course_identifier)
        return cls._default_session_id
# }}}


# {{{ TwoCoursePageTestMixin

class TwoCoursePageTestMixin(TwoCourseTestMixin):
    _course1_default_session_id = None
    _course2_default_session_id = None

    @property
    def flow_id(self):
        raise NotImplementedError

    @classmethod
    def get_default_flow_session_id(cls, course_identifier):
        if course_identifier == cls.course1.identifier:
            if cls._course1_default_session_id is not None:
                return cls._course1_default_session_id
            cls._course1_default_session_id = (
                cls.get_last_session_id(course_identifier))
            return cls._course1_default_session_id
        if course_identifier == cls.course2.identifier:
            if cls._course2_default_session_id is not None:
                return cls._course2_default_session_id
            cls._course2_default_session_id = (
                cls.get_last_session_id(course_identifier))
            return cls._course2_default_session_id

    @classmethod
    def update_default_flow_session_id(cls, course_identifier):
        new_session_id = cls.default_flow_params["flow_session_id"]
        if course_identifier == cls.course1.identifier:
            cls._course1_default_session_id = new_session_id
        elif course_identifier == cls.course2.identifier:
            cls._course2_default_session_id = new_session_id

# }}}


# {{{ SingleCourseQuizPageTestMixin
Dong Zhuang's avatar
Dong Zhuang committed
class SingleCourseQuizPageTestMixin(SingleCoursePageTestMixin):

    skip_code_question = True

    @classmethod_with_client
    def ensure_grading_ui_get(cls, client, page_id):  # noqa: N805
        with cls.temporarily_switch_to_user(
                client, cls.instructor_participation.user):
Dong Zhuang's avatar
Dong Zhuang committed
            url = cls.get_page_grading_url_by_page_id(page_id)
            resp = client.get(url)
Dong Zhuang's avatar
Dong Zhuang committed
            assert resp.status_code == 200

    @classmethod_with_client
    def ensure_analytic_page_get(cls, client, group_id, page_id):  # noqa: N805
        with cls.temporarily_switch_to_user(
                client, cls.instructor_participation.user):
Dong Zhuang's avatar
Dong Zhuang committed
            resp = cls.get_flow_page_analytics(
                    client,
                    flow_id=cls.flow_id, group_id=group_id,
                    page_id=page_id)
Dong Zhuang's avatar
Dong Zhuang committed
            assert resp.status_code == 200

    @classmethod_with_client
Dong Zhuang's avatar
Dong Zhuang committed
    def ensure_download_submission(
            cls, client, group_id, page_id, *,  # noqa: N805
            dl_file_extension=None, file_with_ext_count=None):
        with cls.temporarily_switch_to_user(
                client, cls.instructor_participation.user):
            group_page_id = f"{group_id}/{page_id}"
Dong Zhuang's avatar
Dong Zhuang committed
            resp = cls.post_download_all_submissions_by_group_page_id(
                    client,
                    group_page_id=group_page_id, flow_id=cls.flow_id)
Dong Zhuang's avatar
Dong Zhuang committed
            assert resp.status_code == 200
            prefix, zip_file = resp["Content-Disposition"].split("=")
Dong Zhuang's avatar
Dong Zhuang committed
            assert prefix == "attachment; filename"
            assert resp.get("Content-Type") == "application/zip"
Dong Zhuang's avatar
Dong Zhuang committed
            if dl_file_extension:
                buf = io.BytesIO(resp.content)
Dong Zhuang's avatar
Dong Zhuang committed
                import zipfile
                with zipfile.ZipFile(buf, "r") as zf:
Dong Zhuang's avatar
Dong Zhuang committed
                    assert zf.testzip() is None
                    # todo: make more assertions in terms of file content

                    for f in zf.filelist:
                        assert f.file_size > 0

                    if file_with_ext_count is None:
                        assert len([f for f in zf.filelist if
                                    f.filename.endswith(dl_file_extension)]) > 0, \
                            ("The zipped file unexpectedly didn't contain "
                             f"file with extension '{dl_file_extension}', "
                             "the actual file list "
                             f"is {[f.filename for f in zf.filelist]!r}")
                    else:
                        assert (
                                len([f for f in zf.filelist if
                                     f.filename.endswith(dl_file_extension)])
                                == file_with_ext_count), \
                            ("The zipped file unexpectedly didn't contain "
                             "%d files with extension '%s', the actual file list "
                             "is %s" % (
                                 file_with_ext_count,
                                 dl_file_extension,
                                 repr([f.filename for f in zf.filelist])))
Dong Zhuang's avatar
Dong Zhuang committed

    @classmethod_with_client
Dong Zhuang's avatar
Dong Zhuang committed
    def submit_page_answer_by_ordinal_and_test(
Andreas Klöckner's avatar
Andreas Klöckner committed
            cls, client, page_ordinal, *,  # noqa: N805
            use_correct_answer=True, answer_data=None,
Dong Zhuang's avatar
Dong Zhuang committed
            skip_code_question=True,
            expected_grades=None, expected_post_answer_status_code=200,
            do_grading=False, do_human_grade=False, grade_data=None,
            grade_data_extra_kwargs=None,
            dl_file_extension=None,
            ensure_grading_ui_get_before_grading=False,
            ensure_grading_ui_get_after_grading=False,
            ensure_analytic_page_get_before_submission=False,
            ensure_analytic_page_get_after_submission=False,
            ensure_analytic_page_get_before_grading=False,
            ensure_analytic_page_get_after_grading=False,
            ensure_download_before_submission=False,
            ensure_download_after_submission=False,
            ensure_download_before_grading=False,
            ensure_download_after_grading=False,
            dl_file_with_ext_count=None):
Dong Zhuang's avatar
Dong Zhuang committed
        page_id = cls.get_page_id_via_page_oridnal(page_ordinal)

        return cls.submit_page_answer_by_page_id_and_test(
            client, page_id,
            use_correct_answer=use_correct_answer,
            answer_data=answer_data, skip_code_question=skip_code_question,
            expected_grades=expected_grades,
            expected_post_answer_status_code=expected_post_answer_status_code,
            do_grading=do_grading, do_human_grade=do_human_grade,
            grade_data=grade_data, grade_data_extra_kwargs=grade_data_extra_kwargs,
            dl_file_extension=dl_file_extension,
            ensure_grading_ui_get_before_grading=(
                ensure_grading_ui_get_before_grading),
            ensure_grading_ui_get_after_grading=ensure_grading_ui_get_after_grading,
            ensure_analytic_page_get_before_submission=(
                ensure_analytic_page_get_before_submission),
            ensure_analytic_page_get_after_submission=(
                ensure_analytic_page_get_after_submission),
            ensure_analytic_page_get_before_grading=(
                ensure_analytic_page_get_before_grading),
            ensure_analytic_page_get_after_grading=(
                ensure_analytic_page_get_after_grading),
            ensure_download_before_submission=ensure_download_before_submission,
            ensure_download_after_submission=ensure_download_after_submission,
            ensure_download_before_grading=ensure_download_before_grading,
            ensure_download_after_grading=ensure_download_after_grading,
            dl_file_with_ext_count=dl_file_with_ext_count)

    @classmethod_with_client
Dong Zhuang's avatar
Dong Zhuang committed
    def submit_page_answer_by_page_id_and_test(
Andreas Klöckner's avatar
Andreas Klöckner committed
            cls, client, page_id, *,  # noqa: N805
            use_correct_answer=True, answer_data=None,
Dong Zhuang's avatar
Dong Zhuang committed
            skip_code_question=True,
            expected_grades=None, expected_post_answer_status_code=200,
            do_grading=False, do_human_grade=False, grade_data=None,
            grade_data_extra_kwargs=None,
            dl_file_extension=None,
            ensure_grading_ui_get_before_grading=False,
            ensure_grading_ui_get_after_grading=False,
            ensure_analytic_page_get_before_submission=False,
            ensure_analytic_page_get_after_submission=False,
            ensure_analytic_page_get_before_grading=False,
            ensure_analytic_page_get_after_grading=False,
            ensure_download_before_submission=False,
            ensure_download_after_submission=False,
            ensure_download_before_grading=False,
            ensure_download_after_grading=False,
            dl_file_with_ext_count=None):
Dong Zhuang's avatar
Dong Zhuang committed

        if answer_data is not None:
            assert isinstance(answer_data, dict)
            use_correct_answer = False

        submit_answer_response = None
        post_grade_response = None

        for page_tuple in TEST_PAGE_TUPLE:
            if skip_code_question and page_tuple.need_runpy:
                continue
            if page_id == page_tuple.page_id:
                group_id = page_tuple.group_id
                if ensure_grading_ui_get_before_grading:
                    cls.ensure_grading_ui_get(client, page_id)
Dong Zhuang's avatar
Dong Zhuang committed

                if ensure_analytic_page_get_before_submission:
                    cls.ensure_analytic_page_get(client, group_id, page_id)
Dong Zhuang's avatar
Dong Zhuang committed

                if ensure_download_before_submission:
                    cls.ensure_download_submission(client, group_id, page_id)
Dong Zhuang's avatar
Dong Zhuang committed

                if page_tuple.correct_answer is not None:

                    if answer_data is None:
                        answer_data = page_tuple.correct_answer

                    if page_id in ["anyup", "proof_upload"]:
Dong Zhuang's avatar
Dong Zhuang committed
                        file_path = answer_data["uploaded_file"]
                        if not file_path:
                            # submitting an empty answer
                            submit_answer_response = (
                                cls.post_answer_by_page_id(
                                    client, page_id, answer_data))
Dong Zhuang's avatar
Dong Zhuang committed
                        else:
                            if isinstance(file_path, list):
                                file_path, = file_path

                            file_path = file_path.strip()
                            with open(file_path, "rb") as fp:
Dong Zhuang's avatar
Dong Zhuang committed
                                answer_data = {"uploaded_file": fp}
                                submit_answer_response = (
                                    cls.post_answer_by_page_id(client,
Dong Zhuang's avatar
Dong Zhuang committed
                                        page_id, answer_data))
                    else:
                        submit_answer_response = (
                            cls.post_answer_by_page_id(client, page_id, answer_data))
Dong Zhuang's avatar
Dong Zhuang committed

Dong Zhuang's avatar
Dong Zhuang committed
                    # Fixed #514
                    # https://github.com/inducer/relate/issues/514
                    submit_answer_response.context["form"].as_p()

Dong Zhuang's avatar
Dong Zhuang committed
                    assert (submit_answer_response.status_code
                            == expected_post_answer_status_code)
Dong Zhuang's avatar
Dong Zhuang committed
                    if ensure_analytic_page_get_after_submission:
                        cls.ensure_analytic_page_get(client, group_id, page_id)
Dong Zhuang's avatar
Dong Zhuang committed

                    if ensure_download_after_submission:
                        cls.ensure_download_submission(client, group_id, page_id)
Dong Zhuang's avatar
Dong Zhuang committed

                if not do_grading:
                    break

                assert cls.end_flow(client).status_code == 200
Dong Zhuang's avatar
Dong Zhuang committed

                if ensure_analytic_page_get_before_grading:
                    cls.ensure_analytic_page_get(client, group_id, page_id)
Dong Zhuang's avatar
Dong Zhuang committed

                if ensure_download_before_grading:
                    cls.ensure_download_submission(client, group_id, page_id)
Dong Zhuang's avatar
Dong Zhuang committed

                if page_tuple.correct_answer is not None:
                    if use_correct_answer:
                        expected_grades = page_tuple.full_points

                    if page_tuple.need_human_grade:
                        if not do_human_grade:
                            cls.assertSessionScoreEqual(None)
                            break
                        if grade_data is not None:
                            assert isinstance(grade_data, dict)
                        else:
                            grade_data = page_tuple.grade_data.copy()

                        if grade_data_extra_kwargs:
                            assert isinstance(grade_data_extra_kwargs, dict)
                            grade_data.update(grade_data_extra_kwargs)

                        post_grade_response = cls.post_grade_by_page_id(
                            client, page_id, grade_data)
Dong Zhuang's avatar
Dong Zhuang committed
                    cls.assertSessionScoreEqual(expected_grades)

                    if not dl_file_extension:
                        dl_file_extension = page_tuple.dl_file_extension

                    if ensure_download_after_grading:
                        cls.ensure_download_submission(
                                client,
                                group_id, page_id,
                                dl_file_extension=dl_file_extension,
                                file_with_ext_count=dl_file_with_ext_count)
Dong Zhuang's avatar
Dong Zhuang committed

                if ensure_analytic_page_get_after_grading:
                    cls.ensure_analytic_page_get(client, group_id, page_id)
Dong Zhuang's avatar
Dong Zhuang committed

                if ensure_grading_ui_get_after_grading:
                    cls.ensure_grading_ui_get(client, page_id)
Dong Zhuang's avatar
Dong Zhuang committed

        return submit_answer_response, post_grade_response

    def default_submit_page_answer_by_page_id_and_test(self, page_id,
                                                       answer_data=None,
                                                       expected_grade=None,
                                                       do_grading=True,
                                                       grade_data=None,
                                                       grade_data_extra_kwargs=None,
                                                       ):
        return self.submit_page_answer_by_page_id_and_test(
            page_id, answer_data=answer_data,
            skip_code_question=self.skip_code_question,
            expected_grades=expected_grade, expected_post_answer_status_code=200,
            do_grading=do_grading, do_human_grade=True, grade_data=grade_data,
            grade_data_extra_kwargs=grade_data_extra_kwargs,
            ensure_grading_ui_get_before_grading=True,
            ensure_grading_ui_get_after_grading=True,
            ensure_analytic_page_get_before_submission=True,
            ensure_analytic_page_get_after_submission=True,
            ensure_analytic_page_get_before_grading=True,
            ensure_analytic_page_get_after_grading=True,
            ensure_download_before_submission=True,
            ensure_download_after_submission=True,
            ensure_download_before_grading=True,
            ensure_download_after_grading=True)

    @classmethod_with_client
Dong Zhuang's avatar
Dong Zhuang committed
    def submit_page_human_grading_by_page_id_and_test(
            cls, client, page_id, *,  # noqa: N805
Dong Zhuang's avatar
Dong Zhuang committed
            expected_post_grading_status_code=200,
Dong Zhuang's avatar
Dong Zhuang committed
            grade_data=None,
            expected_grades=None,
            do_session_score_equal_assertion=True,
Dong Zhuang's avatar
Dong Zhuang committed
            grade_data_extra_kwargs=None,
            force_login_instructor=True,
            ensure_grading_ui_get_before_grading=False,
            ensure_grading_ui_get_after_grading=False,
            ensure_analytic_page_get_before_grading=False,
            ensure_analytic_page_get_after_grading=False,
            ensure_download_before_grading=False,
            ensure_download_after_grading=False):

Dong Zhuang's avatar
Dong Zhuang committed
        # this helper is expected to be used when the session is finished

Dong Zhuang's avatar
Dong Zhuang committed
        post_grade_response = None

        for page_tuple in TEST_PAGE_TUPLE:
            if page_id == page_tuple.page_id:
                group_id = page_tuple.group_id
                if ensure_grading_ui_get_before_grading:
                    cls.ensure_grading_ui_get(page_id)

                if ensure_analytic_page_get_before_grading:
                    cls.ensure_analytic_page_get(group_id, page_id)

                if ensure_download_before_grading:
                    cls.ensure_download_submission(group_id, page_id)

                if not page_tuple.need_human_grade:
                    break

                assign_full_grades = True

                if grade_data is not None:
                    assert isinstance(grade_data, dict)
                    assign_full_grades = False
                else:
                    grade_data = page_tuple.grade_data.copy()

                if assign_full_grades:
                    expected_grades = page_tuple.full_points

                if grade_data_extra_kwargs:
                    assert isinstance(grade_data_extra_kwargs, dict)
                    grade_data.update(grade_data_extra_kwargs)

                post_grade_response = cls.post_grade_by_page_id(
                        client,
                        page_id, grade_data,
                        force_login_instructor=force_login_instructor)
Dong Zhuang's avatar
Dong Zhuang committed

                assert (post_grade_response.status_code
                        == expected_post_grading_status_code)

                if post_grade_response.status_code == 200:
                    if do_session_score_equal_assertion:
Dong Zhuang's avatar
Dong Zhuang committed
                        cls.assertSessionScoreEqual(expected_grades)
Dong Zhuang's avatar
Dong Zhuang committed

                if ensure_download_after_grading:
                    cls.ensure_download_submission(group_id, page_id)

                if ensure_analytic_page_get_after_grading:
                    cls.ensure_analytic_page_get(group_id, page_id)

                if ensure_grading_ui_get_after_grading:
                    cls.ensure_grading_ui_get(page_id)

        return post_grade_response

# }}}


# {{{ MockAddMessageMixing
Dong Zhuang's avatar
Dong Zhuang committed

class MockAddMessageMixing:
    """
    The mixing for testing django.contrib.messages.add_message
    """

    def setUp(self):
Dong Zhuang's avatar
Dong Zhuang committed
        self._fake_add_message_path = "django.contrib.messages.add_message"
        fake_add_message = mock.patch(self._fake_add_message_path)
        self._mock_add_message = fake_add_message.start()
        self.addCleanup(fake_add_message.stop)
Dong Zhuang's avatar
Dong Zhuang committed
    def _get_added_messages(self, join=True):
        try:
            msgs = [
                f"'{arg[2]!s}'"
Dong Zhuang's avatar
Dong Zhuang committed
                for arg, _ in self._mock_add_message.call_args_list]
        except IndexError:
            self.fail(f"{self._fake_add_message_path} is unexpectedly not called.")
Dong Zhuang's avatar
Dong Zhuang committed
        else:
            if join:
                return "; ".join(msgs)
            return msgs

    def assertAddMessageCallCount(self, expected_call_count, reset=False):  # noqa
Dong Zhuang's avatar
Dong Zhuang committed
        fail_msg = (
            "%s is unexpectedly called %d times, instead of %d times." %
            (self._fake_add_message_path, self._mock_add_message.call_count,
             expected_call_count))
        if self._mock_add_message.call_count > 0:
            fail_msg += (
                f"The called messages are: {self._get_added_messages(join=False)!r}")
Dong Zhuang's avatar
Dong Zhuang committed
            self._mock_add_message.call_count, expected_call_count, msg=fail_msg)
        if reset:
            self._mock_add_message.reset_mock()

    def assertAddMessageCalledWith(self, expected_messages, reset=True):  # noqa
Dong Zhuang's avatar
Dong Zhuang committed
        joined_msgs = self._get_added_messages()

        if not isinstance(expected_messages, list):
            expected_messages = [expected_messages]

        not_called = []
        for msg in expected_messages:
Dong Zhuang's avatar
Dong Zhuang committed
            if msg not in joined_msgs:
            fail_msg = f"{not_called!r} unexpectedly not added in messages. "
Dong Zhuang's avatar
Dong Zhuang committed
            if joined_msgs:
                fail_msg += f'the actual message are "{joined_msgs}"'
Dong Zhuang's avatar
Dong Zhuang committed
            self.fail(fail_msg)
        if reset:
            self._mock_add_message.reset_mock()

Dong Zhuang's avatar
Dong Zhuang committed
    def assertAddMessageNotCalledWith(self, expected_messages, reset=False):  # noqa
        joined_msgs = self._get_added_messages()

        if not isinstance(expected_messages, list):
            expected_messages = [expected_messages]

        called = []
        for msg in expected_messages:
            if msg in joined_msgs:
                called.append(msg)

        if called:
            fail_msg = f"{called!r} unexpectedly added in messages. "
            fail_msg += f'the actual message are "{joined_msgs}"'
Dong Zhuang's avatar
Dong Zhuang committed
            self.fail(fail_msg)
        if reset:
            self._mock_add_message.reset_mock()

    def reset_add_message_mock(self):
        self._mock_add_message.reset_mock()

# }}}


# {{{ SubprocessRunpyContainerMixin
class SubprocessRunpyContainerMixin:
Dong Zhuang's avatar
Dong Zhuang committed
    """
    This mixin is used to fake a runpy container, only needed when
    the TestCase include test(s) for code questions
    """
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
Dong Zhuang's avatar
Dong Zhuang committed

        python_executable = os.getenv("PY_EXE")

        if not python_executable:
            python_executable = sys.executable

        import subprocess
        args = [python_executable,
                os.path.abspath(
                    os.path.join(
                        os.path.dirname(__file__), os.pardir,
Neal Davis's avatar
Neal Davis committed
                        "docker-image-run-py", "runcode")),
Dong Zhuang's avatar
Dong Zhuang committed
                ]
        cls.faked_container_process = subprocess.Popen(
            args,
            stdout=subprocess.DEVNULL,

            # because runpy prints to stderr
            stderr=subprocess.DEVNULL
        )

        self.faked_container_patch = mock.patch(
Neal Davis's avatar
Neal Davis committed
            "course.page.code.SPAWN_CONTAINERS", False)
        self.faked_container_patch.start()
        self.addCleanup(self.faked_container_patch.stop)
Dong Zhuang's avatar
Dong Zhuang committed

    @classmethod
    def tearDownClass(cls):
        super().tearDownClass()
Neal Davis's avatar
Neal Davis committed
        from course.page.code import SPAWN_CONTAINERS
Andreas Klöckner's avatar
Andreas Klöckner committed

Neal Davis's avatar
Neal Davis committed
        # Make sure SPAWN_CONTAINERS is reset to True
        assert SPAWN_CONTAINERS
        if sys.platform.startswith("win"):
            # Without these lines, tests on Appveyor hanged when all tests
            # finished.
            # However, On nix platforms, these lines resulted in test
            # failure when there were more than one TestCases which were using
            # this mixin. So we don't kill the subprocess, and it won't bring
            # bad side effects to remainder tests.
            cls.faked_container_process.kill()

def improperly_configured_cache_patch():
    # can be used as context manager or decorator
    built_in_import_path = "builtins.__import__"
    import builtins
    built_in_import = builtins.__import__

    def my_disable_cache_import(name, globals=None, locals=None, fromlist=(),
                                level=0):
        if name == "django.core.cache":
            raise ImproperlyConfigured()
        return built_in_import(name, globals, locals, fromlist, level)

    return mock.patch(built_in_import_path, side_effect=my_disable_cache_import)

# {{{ admin

ADMIN_TWO_COURSE_SETUP_LIST = deepcopy(TWO_COURSE_SETUP_LIST)
# switch roles
ADMIN_TWO_COURSE_SETUP_LIST[1]["participations"][0]["role_identifier"] = "ta"
ADMIN_TWO_COURSE_SETUP_LIST[1]["participations"][1]["role_identifier"] = "instructor"


class AdminTestMixin(TwoCourseTestMixin):
    courses_setup_list = ADMIN_TWO_COURSE_SETUP_LIST
    none_participation_user_create_kwarg_list = (
        NONE_PARTICIPATION_USER_CREATE_KWARG_LIST)

    @classmethod
    def setUpTestData(cls):
        super().setUpTestData()

        # create 2 participation (with new user) for course1
        from tests.factories import ParticipationFactory

        cls.course1_student_participation2 = (
            ParticipationFactory.create(course=cls.course1))
        cls.course1_student_participation3 = (
            ParticipationFactory.create(course=cls.course1))
        cls.instructor1 = cls.course1_instructor_participation.user
        cls.instructor2 = cls.course2_instructor_participation.user
        assert cls.instructor1 != cls.instructor2

        # grant all admin permissions to instructors
        from django.contrib.auth.models import Permission

        for user in [cls.instructor1, cls.instructor2]:
            user.is_staff = True
            user.save()
            for perm in Permission.objects.all():
                user.user_permissions.add(perm)

    @classmethod
    def get_admin_change_list_view_url(cls, app_name, model_name):
        return reverse(f"admin:{app_name}_{model_name}_changelist")

    @classmethod
    def get_admin_change_view_url(cls, app_name, model_name, args=None):
        if args is None:
            args = []
        return reverse(f"admin:{app_name}_{model_name}_change", args=args)
Dong Zhuang's avatar
Dong Zhuang committed
    @classmethod
    def get_admin_add_view_url(cls, app_name, model_name, args=None):
        if args is None:
            args = []
        return reverse(f"admin:{app_name}_{model_name}_add", args=args)
Dong Zhuang's avatar
Dong Zhuang committed

    def get_admin_form_fields(self, response):
        """
        Return a list of AdminFields for the AdminForm in the response.
        """
        admin_form = response.context["adminform"]
        fieldsets = list(admin_form)

        field_lines = []
        for fieldset in fieldsets:
            field_lines += list(fieldset)

        fields = []
        for field_line in field_lines:
            fields += list(field_line)

        return fields

    def get_admin_form_fields_names(self, response):
        return [f.field.name for f in self.get_admin_form_fields(response)]

    def get_changelist(self, request, model, model_admin):
        from django.contrib.admin.views.main import ChangeList
        return ChangeList(
            request, model, model_admin.list_display,
            model_admin.list_display_links, model_admin.get_list_filter(request),
            model_admin.date_hierarchy, model_admin.search_fields,
            model_admin.list_select_related, model_admin.list_per_page,
            model_admin.list_max_show_all, model_admin.list_editable,
            model_admin=model_admin,
Andreas Klöckner's avatar
Andreas Klöckner committed
            sortable_by=model_admin.sortable_by,
            search_help_text="(no help text)",
        )

    def get_filterspec_list(self, request, changelist=None, model=None,
            assert request and model and model_admin
            changelist = self.get_changelist(request, model, model_admin)

        filterspecs = changelist.get_filters(request)[0]
        filterspec_list = []
        for filterspec in filterspecs:
            choices = tuple(c["display"] for c in filterspec.choices(changelist))
            filterspec_list.append(choices)

        return filterspec_list

# }}}

ifaint's avatar
ifaint committed
# {{{ api

class APITestMixin(SingleCoursePageTestMixin):
    # test manage_authentication_tokens
    flow_id = QUIZ_FLOW_ID
    force_login_student_for_each_test = False
    default_token_hash_str = "my0token0string"

    def get_get_flow_session_api_url(
            self, course_identifier=None, flow_id=None,
            auto_add_default_flow_id=True):
        course_identifier = (
            course_identifier or self.get_default_course_identifier())
        if auto_add_default_flow_id:
            flow_id = flow_id or self.flow_id
        kwargs = {"course_identifier": course_identifier}

        url = reverse("relate-course_get_flow_session", kwargs=kwargs)

        if flow_id:
            url += f"?flow_id={flow_id}"
ifaint's avatar
ifaint committed
        return url

ifaint's avatar
ifaint committed
    def get_get_flow_session_content_url(
            self, course_identifier=None, flow_session_id=None,
            auto_add_default_flow_session_id=True):
        course_identifier = (
            course_identifier or self.get_default_course_identifier())
        if auto_add_default_flow_session_id:
            flow_session_id = (
                flow_session_id
                or self.get_default_flow_session_id(course_identifier))
        kwargs = {"course_identifier": course_identifier}

        url = reverse("relate-course_get_flow_session_content", kwargs=kwargs)

        if flow_session_id:
            url += f"?flow_session_id={flow_session_id}"
ifaint's avatar
ifaint committed
        return url

ifaint's avatar
ifaint committed
    def create_token(self, token_hash_str=None, participation=None, **kwargs):
        token_hash_str = token_hash_str or self.default_token_hash_str
        participation = participation or self.instructor_participation

        from tests.factories import AuthenticationTokenFactory
        with mock.patch("tests.factories.make_sign_in_key") as mock_mk_sign_in_key:
            mock_mk_sign_in_key.return_value = token_hash_str
            token = AuthenticationTokenFactory(
                user=participation.user,
                participation=participation,
                **kwargs
            )
            return token

    def create_basic_auth(self, token=None, participation=None, user=None):
        participation = participation or self.instructor_participation
        user = user or participation.user
        token = token or self.create_token(participation=participation)
        basic_auth_str = f"{user.username}:{token.id}_{self.default_token_hash_str}"
ifaint's avatar
ifaint committed

        from base64 import b64encode
        return b64encode(basic_auth_str.encode("utf-8")).decode()


# }}}


# {{{ HackRepoMixin

class HackRepoMixin:

    # This need to be configured when the module tested imported get_repo_blob
    # at module level
    get_repo_blob_patching_path = "course.content.get_repo_blob"

    @classmethod
    def setUpTestData(cls):
        super().setUpTestData()
            def __init__(self, yaml_file_name):
                with open(os.path.join(FAKED_YAML_PATH, yaml_file_name), "rb") as f:
                    data = f.read()
                self.data = data

        def get_repo_side_effect(repo, full_name, commit_sha):
            commit_sha_path_maps = COMMIT_SHA_MAP.get(full_name)
            if commit_sha_path_maps:
                assert isinstance(commit_sha_path_maps, list)
                for cs_map in commit_sha_path_maps:
                    if commit_sha.decode() in cs_map:
                        path = cs_map[commit_sha.decode()]["path"]
                        return Blob(path)

            return get_repo_blob(repo, full_name, repo[b"HEAD"].id)

        cls.batch_fake_get_repo_blob = mock.patch(cls.get_repo_blob_patching_path)
        cls.mock_get_repo_blob = cls.batch_fake_get_repo_blob.start()
        cls.mock_get_repo_blob.side_effect = get_repo_side_effect

    @classmethod
    def tearDownClass(cls):
        # This must be done to avoid inconsistency
        super().tearDownClass()
        cls.batch_fake_get_repo_blob.stop()

    def get_current_page_ids(self):
        current_sha = self.course.active_git_commit_sha
        for commit_sha_path_maps in COMMIT_SHA_MAP.values():
            for cs_map in commit_sha_path_maps:
                if current_sha in cs_map:
                    return cs_map[current_sha]["page_ids"]

        raise ValueError("Page_ids for that commit_sha doesn't exist")

    def assertGradeInfoEqual(self, resp, expected_grade_info_dict=None):  # noqa
        grade_info = resp.context["grade_info"]

        assert isinstance(grade_info, GradeInfo)
        if not expected_grade_info_dict:
            import json
            error_msg = ("\n{}".format(json.dumps(OrderedDict(
                sorted(grade_info.__dict__.items())),
            error_msg = error_msg.replace("null", "None")
            self.fail(error_msg)

        assert isinstance(expected_grade_info_dict, dict)

        grade_info_dict = grade_info.__dict__
        not_match_infos = []
        for k in grade_info_dict.keys():
            if grade_info_dict[k] != expected_grade_info_dict[k]:
                not_match_infos.append(
                    f"'{k}' is expected to be {expected_grade_info_dict[k]!s}, while got {grade_info_dict[k]!s}")  # noqa: E501

        if not_match_infos:
            self.fail("\n".join(not_match_infos))