def test_handle_username_dedup(self, mock_task, mock_get_programs):
        """Verify that only one task is enqueued for a user with multiple eligible certs."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id),
                        factories.RunMode(course_key=self.alternate_course_id),
                    ]),
                ]),
        ]
        mock_get_programs.return_value = data

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.alternate_course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        mock_task.assert_called_once_with(self.alice.username)
Exemple #2
0
    def test_single_program_engagement(self):
        """
        Verify that correct program is returned when the user has a single enrollment
        appearing in one program.
        """
        course_id = 'org/course/run'
        data = [
            factories.Program(organizations=[factories.Organization()],
                              course_codes=[
                                  factories.CourseCode(run_modes=[
                                      factories.RunMode(course_key=course_id),
                                  ]),
                              ]),
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[factories.RunMode()]),
                ]),
        ]
        self._mock_programs_api(data)

        self._create_enrollments(course_id)
        meter = utils.ProgramProgressMeter(self.user)

        program = data[0]
        self.assertEqual(meter.engaged_programs, [program])
        self._assert_progress(
            meter,
            factories.Progress(id=program['id'],
                               in_progress=self._extract_names(program, 0)))
        self.assertEqual(meter.completed_programs, [])
Exemple #3
0
    def test_shared_enrollment_engagement(self):
        """
        Verify that correct programs are returned when the user has a single enrollment
        appearing in multiple programs.
        """
        shared_course_id, solo_course_id = 'org/shared-course/run', 'org/solo-course/run'
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=shared_course_id),
                    ]),
                ]),
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=shared_course_id),
                    ]),
                ]),
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=solo_course_id),
                    ]),
                ]),
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[factories.RunMode()]),
                ]),
        ]
        self._mock_programs_api(data)

        # Enrollment for the shared course ID created last (most recently).
        self._create_enrollments(solo_course_id, shared_course_id)
        meter = utils.ProgramProgressMeter(self.user)

        programs = data[:3]
        self.assertEqual(meter.engaged_programs, programs)
        self._assert_progress(
            meter,
            factories.Progress(id=programs[0]['id'],
                               in_progress=self._extract_names(programs[0],
                                                               0)),
            factories.Progress(id=programs[1]['id'],
                               in_progress=self._extract_names(programs[1],
                                                               0)),
            factories.Progress(id=programs[2]['id'],
                               in_progress=self._extract_names(programs[2],
                                                               0)))
        self.assertEqual(meter.completed_programs, [])
    def test_tab_is_enabled_with_nonempty_list(self):
        """
        The programs tab and "new program" button should appear when enabled
        via config, and the results of the program list should display when
        the list is nonempty.
        """
        test_program_values = [('first program', 'org1'),
                               ('second program', 'org2')]

        programs = [
            factories.Program(name=name,
                              organizations=[
                                  factories.Organization(key=org),
                              ],
                              course_codes=[
                                  factories.CourseCode(run_modes=[
                                      factories.RunMode(),
                                  ]),
                              ]) for name, org in test_program_values
        ]

        ProgramsFixture().install_programs(programs)

        self.set_programs_api_configuration(True)

        self.dashboard_page.visit()

        self.assertTrue(self.dashboard_page.is_programs_tab_present())
        self.assertTrue(self.dashboard_page.is_new_program_button_present())
        self.assertFalse(
            self.dashboard_page.is_empty_list_create_button_present())

        results = self.dashboard_page.get_program_list()
        self.assertEqual(results, test_program_values)
Exemple #5
0
    def test_get_completed_programs(self, mock_get_completed_courses):
        """
        Verify that completed programs are found, using the cache when possible.
        """
        course_id = 'org/course/run'
        data = [
            factories.Program(organizations=[factories.Organization()],
                              course_codes=[
                                  factories.CourseCode(run_modes=[
                                      factories.RunMode(course_key=course_id),
                                  ]),
                              ]),
        ]
        self._mock_programs_api(data)

        mock_get_completed_courses.return_value = [{
            'course_id': course_id,
            'mode': MODES.verified
        }]

        for _ in range(2):
            result = tasks.get_completed_programs(self.user)
            self.assertEqual(result, [data[0]['id']])

        # Verify that only one request to programs was made (i.e., the cache was hit).
        self._assert_num_requests(1)
Exemple #6
0
    def setUpClass(cls):
        super(TestProgramListing, cls).setUpClass()

        for name in [
                ProgramsApiConfig.OAUTH2_CLIENT_NAME,
                CredentialsApiConfig.OAUTH2_CLIENT_NAME
        ]:
            ClientFactory(name=name, client_type=CONFIDENTIAL)

        cls.course = CourseFactory()
        organization = programs_factories.Organization()
        run_mode = programs_factories.RunMode(course_key=unicode(
            cls.course.id))  # pylint: disable=no-member
        course_code = programs_factories.CourseCode(run_modes=[run_mode])

        cls.first_program = programs_factories.Program(
            organizations=[organization], course_codes=[course_code])
        cls.second_program = programs_factories.Program(
            organizations=[organization], course_codes=[course_code])

        cls.data = sorted([cls.first_program, cls.second_program],
                          key=cls.program_sort_key)

        cls.marketing_root = urljoin(settings.MKTG_URLS.get('ROOT'),
                                     'xseries').rstrip('/')
    def test_handle_mode_slugs(self, mock_task, mock_get_programs):
        """Verify that mode slugs are taken into account."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id,
                                          mode_slug=MODES.honor),
                    ]),
                ]),
        ]
        mock_get_programs.return_value = data

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        mock_task.assert_called_once_with(self.alice.username)
Exemple #8
0
    def test_nonstandard_run_mode_completion(self, mock_get_completed_courses):
        """
        A valid run mode isn't necessarily verified. Verify that the program can
        still be completed when this is the case.
        """
        course_id = 'org/course/run'
        data = [
            factories.Program(organizations=[factories.Organization()],
                              course_codes=[
                                  factories.CourseCode(run_modes=[
                                      factories.RunMode(course_key=course_id,
                                                        mode_slug=MODES.honor),
                                  ]),
                              ]),
        ]
        self._mock_programs_api(data)

        enrollments = self._create_enrollments(course_id)
        meter = utils.ProgramProgressMeter(self.user, enrollments)

        mock_get_completed_courses.return_value = [
            {
                'course_id': course_id,
                'mode': MODES.honor
            },
        ]

        program = data[0]
        self._assert_progress(
            meter,
            factories.Progress(id=program['id'],
                               completed=self._extract_names(program, 0)))
    def test_handle(self, commit, mock_task, mock_get_programs):
        """Verify that relevant tasks are only enqueued when the commit option is passed."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id),
                    ]),
                ]),
        ]
        mock_get_programs.return_value = data

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.alternate_course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=commit)

        if commit:
            mock_task.assert_called_once_with(self.alice.username)
        else:
            mock_task.assert_not_called()
    def _assert_supplemented(self, actual, **kwargs):
        """DRY helper used to verify that program data is extended correctly."""
        course_overview = CourseOverview.get_from_id(self.course.id)  # pylint: disable=no-member
        run_mode = dict(
            factories.RunMode(
                certificate_url=None,
                course_image_url=course_overview.course_image_url,
                course_key=unicode(self.course.id),  # pylint: disable=no-member
                course_url=reverse('course_root', args=[self.course.id]),  # pylint: disable=no-member
                end_date=self.course.end.replace(tzinfo=utc),
                enrollment_open_date=strftime_localized(
                    utils.DEFAULT_ENROLLMENT_START_DATE, 'SHORT_DATE'),
                is_course_ended=self.course.end < datetime.datetime.now(utc),
                is_enrolled=False,
                is_enrollment_open=True,
                marketing_url=MARKETING_URL,
                start_date=self.course.start.replace(tzinfo=utc),
                upgrade_url=None,
                advertised_start=None),
            **kwargs)
        course_code = factories.CourseCode(
            display_name=self.course_code['display_name'],
            run_modes=[run_mode])
        expected = copy.deepcopy(self.program)
        expected['course_codes'] = [course_code]

        self.assertEqual(actual, expected)
    def setUpClass(cls):
        super(GetProgramsByRunTests, cls).setUpClass()

        cls.user = UserFactory()

        course_keys = [
            CourseKey.from_string('some/course/run'),
            CourseKey.from_string('some/other/run'),
        ]

        cls.enrollments = [
            CourseEnrollmentFactory(user=cls.user, course_id=c)
            for c in course_keys
        ]
        cls.course_ids = [unicode(c) for c in course_keys]

        organization = factories.Organization()
        joint_programs = sorted([
            factories.Program(
                organizations=[organization],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=cls.course_ids[0]),
                    ]),
                ]) for __ in range(2)
        ],
                                key=lambda p: p['name'])

        cls.programs = joint_programs + [
            factories.Program(
                organizations=[organization],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=cls.course_ids[1]),
                    ]),
                ]),
            factories.Program(
                organizations=[organization],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key='yet/another/run'),
                    ]),
                ]),
        ]
    def test_mutiple_program_engagement(self):
        """
        Verify that correct programs are returned in the correct order when the user
        has multiple enrollments.
        """
        first_course_id, second_course_id = 'org/first-course/run', 'org/second-course/run'
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=first_course_id),
                    ]),
                ]),
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=second_course_id),
                    ]),
                ]),
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[factories.RunMode()]),
                ]),
        ]
        self._mock_programs_api(data)

        self._create_enrollments(second_course_id, first_course_id)
        meter = utils.ProgramProgressMeter(self.user)

        self._attach_detail_url(data)
        programs = data[:2]
        self.assertEqual(meter.engaged_programs(), programs)
        self._assert_progress(
            meter,
            factories.Progress(id=programs[0]['id'],
                               in_progress=self._extract_names(programs[0],
                                                               0)),
            factories.Progress(id=programs[1]['id'],
                               in_progress=self._extract_names(programs[1],
                                                               0)))
        self.assertEqual(meter.completed_programs, [])
    def test_handle_passing_status(self, mock_task):
        """Verify that only certificates with a passing status are selected."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id),
                        factories.RunMode(course_key=self.alternate_course_id),
                    ]),
                ]),
        ]
        self._mock_programs_api(data)
        self._link_oauth2_user()

        passing_status = CertificateStatuses.downloadable
        failing_status = CertificateStatuses.notpassing

        self.assertIn(passing_status, CertificateStatuses.PASSED_STATUSES)
        self.assertNotIn(failing_status, CertificateStatuses.PASSED_STATUSES)

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=passing_status,
        )

        # The alternate course is used here to verify that the status and run_mode
        # queries are being ANDed together correctly.
        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.alternate_course_id,
            mode=MODES.verified,
            status=failing_status,
        )

        call_command('backpopulate_program_credentials', commit=True)

        mock_task.assert_called_once_with(self.alice.username)
Exemple #14
0
    def setUpClass(cls):
        super(TestProgramDetails, cls).setUpClass()

        ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME,
                      client_type=CONFIDENTIAL)

        course = CourseFactory()
        organization = programs_factories.Organization()
        run_mode = programs_factories.RunMode(course_key=unicode(course.id))  # pylint: disable=no-member
        course_code = programs_factories.CourseCode(run_modes=[run_mode])

        cls.data = programs_factories.Program(organizations=[organization],
                                              course_codes=[course_code])
Exemple #15
0
    def test_unrelated_program_not_listed(self):
        """Verify that unrelated programs don't appear in the listing."""
        run_mode = programs_factories.RunMode(
            course_key='some/nonexistent/run')
        course_code = programs_factories.CourseCode(run_modes=[run_mode])

        unrelated_program = programs_factories.Program(
            organizations=[self.organization], course_codes=[course_code])

        self.mock_programs_api(self.programs + [unrelated_program])

        response = self.client.get(self.url)
        self.assert_related_programs(response)
        self.assertNotContains(response, unrelated_program['name'])
Exemple #16
0
    def test_no_enrollments(self):
        """Verify behavior when programs exist, but no relevant enrollments do."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[factories.RunMode()]),
                ]),
        ]
        self._mock_programs_api(data)

        meter = utils.ProgramProgressMeter(self.user, [])

        self.assertEqual(meter.engaged_programs, [])
        self._assert_progress(meter)
    def setUp(self):
        super(TestProgramDetails, self).setUp()

        self.details_page = reverse('program_details_view',
                                    args=[self.program_id])

        self.user = UserFactory()
        self.client.login(username=self.user.username, password=self.password)

        ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME,
                      client_type=CONFIDENTIAL)

        self.organization = factories.Organization()
        self.run_mode = factories.RunMode(course_key=unicode(self.course.id))  # pylint: disable=no-member
        self.course_code = factories.CourseCode(run_modes=[self.run_mode])
        self.data = factories.Program(organizations=[self.organization],
                                      course_codes=[self.course_code])
Exemple #18
0
    def setUpClass(cls):
        super(RelatedProgramsTests, cls).setUpClass()

        cls.user = UserFactory()
        cls.course = CourseFactory()
        cls.enrollment = CourseEnrollmentFactory(user=cls.user, course_id=cls.course.id)  # pylint: disable=no-member
        ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL)

        cls.organization = programs_factories.Organization()
        run_mode = programs_factories.RunMode(course_key=unicode(cls.course.id))  # pylint: disable=no-member
        course_code = programs_factories.CourseCode(run_modes=[run_mode])

        cls.programs = [
            programs_factories.Program(
                organizations=[cls.organization],
                course_codes=[course_code]
            ) for __ in range(2)
        ]
Exemple #19
0
    def setUp(self):
        super(TestSupplementProgramData, self).setUp()

        self.user = UserFactory()
        self.client.login(username=self.user.username, password=self.password)

        ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME,
                      client_type=CONFIDENTIAL)

        self.course = CourseFactory()
        self.course.start = timezone.now() - datetime.timedelta(days=1)
        self.course.end = timezone.now() + datetime.timedelta(days=1)
        self.course = self.update_course(self.course, self.user.id)  # pylint: disable=no-member

        self.organization = factories.Organization()
        self.run_mode = factories.RunMode(course_key=unicode(self.course.id))  # pylint: disable=no-member
        self.course_code = factories.CourseCode(run_modes=[self.run_mode])
        self.program = factories.Program(organizations=[self.organization],
                                         course_codes=[self.course_code])
Exemple #20
0
    def create_program(self, program_id=None, course_id=None):
        """DRY helper for creating test program data."""
        course_id = course_id if course_id else self.course_id

        run_mode = factories.RunMode(course_key=course_id)
        course_code = factories.CourseCode(run_modes=[run_mode])
        org = factories.Organization(key=self.course_info['org'])

        if program_id:
            program = factories.Program(id=program_id,
                                        status='active',
                                        organizations=[org],
                                        course_codes=[course_code])
        else:
            program = factories.Program(status='active',
                                        organizations=[org],
                                        course_codes=[course_code])

        return program
    def test_handle_enqueue_failure(self, mock_log, mock_task,
                                    mock_get_programs):
        """Verify that failure to enqueue a task doesn't halt execution."""
        def side_effect(username):
            """Simulate failure to enqueue a task."""
            if username == self.alice.username:
                raise Exception

        mock_task.side_effect = side_effect

        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id),
                    ]),
                ]),
        ]
        mock_get_programs.return_value = data

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        self.assertTrue(mock_log.called)

        calls = [mock.call(self.alice.username), mock.call(self.bob.username)]
        mock_task.assert_has_calls(calls, any_order=True)
Exemple #22
0
    def _assert_supplemented(self, actual, **kwargs):
        """DRY helper used to verify that program data is extended correctly."""
        course_overview = CourseOverview.get_from_id(self.course.id)  # pylint: disable=no-member
        run_mode = dict(
            factories.RunMode(
                course_key=unicode(self.course.id),  # pylint: disable=no-member
                course_url=reverse('course_root', args=[self.course.id]),  # pylint: disable=no-member
                course_image_url=course_overview.course_image_url,
                start_date=strftime_localized(self.course.start, 'SHORT_DATE'),
                end_date=strftime_localized(self.course.end, 'SHORT_DATE'),
                is_course_ended=self.course.end < timezone.now(),
                is_enrolled=False,
                is_enrollment_open=True,
                marketing_url='',
            ),
            **kwargs
        )
        course_code = factories.CourseCode(display_name=self.course_code['display_name'], run_modes=[run_mode])
        expected = copy.deepcopy(self.program)
        expected['course_codes'] = [course_code]

        self.assertEqual(actual, expected)
Exemple #23
0
    def test_completed_programs(self, mock_get_completed_courses):
        """Verify that completed programs are correctly identified."""
        program_count, course_code_count, run_mode_count = 3, 2, 2
        data = [
            factories.Program(organizations=[factories.Organization()],
                              course_codes=[
                                  factories.CourseCode(run_modes=[
                                      factories.RunMode()
                                      for _ in range(run_mode_count)
                                  ]) for _ in range(course_code_count)
                              ]) for _ in range(program_count)
        ]
        self._mock_programs_api(data)

        program_ids = []
        course_ids = []
        for program in data:
            program_ids.append(program['id'])

            for course_code in program['course_codes']:
                for run_mode in course_code['run_modes']:
                    course_ids.append(run_mode['course_key'])

        # Verify that no programs are complete.
        meter = utils.ProgramProgressMeter(self.user)
        self.assertEqual(meter.completed_programs, [])

        # "Complete" all programs.
        self._create_enrollments(*course_ids)
        mock_get_completed_courses.return_value = [{
            'course_id': course_id,
            'mode': MODES.verified
        } for course_id in course_ids]

        # Verify that all programs are complete.
        meter = utils.ProgramProgressMeter(self.user)
        self.assertEqual(meter.completed_programs, program_ids)
Exemple #24
0
    def _assert_supplemented(self,
                             actual,
                             is_enrolled=False,
                             is_enrollment_open=True):
        """DRY helper used to verify that program data is extended correctly."""
        course_overview = CourseOverview.get_from_id(self.course.id)  # pylint: disable=no-member

        run_mode = factories.RunMode(
            course_key=unicode(self.course.id),  # pylint: disable=no-member
            course_url=reverse('course_root', args=[self.course.id]),  # pylint: disable=no-member
            course_image_url=course_overview.course_image_url,
            start_date=self.course.start.strftime(self.human_friendly_format),
            end_date=self.course.end.strftime(self.human_friendly_format),
            is_enrolled=is_enrolled,
            is_enrollment_open=is_enrollment_open,
            marketing_url='',
        )
        course_code = factories.CourseCode(
            display_name=self.course_code['display_name'],
            run_modes=[run_mode])
        expected = copy.deepcopy(self.program)
        expected['course_codes'] = [course_code]

        self.assertEqual(actual, expected)
Exemple #25
0
    def install_programs(self, fake_programs):
        """
        Sets the response data for the programs list endpoint.

        At present, `fake_programs` must be a iterable of FakeProgram named tuples.
        """
        programs = []
        for program in fake_programs:
            run_mode = factories.RunMode(course_key=program.course_id)
            course_code = factories.CourseCode(run_modes=[run_mode])
            org = factories.Organization(key=program.org_key)

            program = factories.Program(name=program.name,
                                        status=program.status,
                                        organizations=[org],
                                        course_codes=[course_code])
            programs.append(program)

        api_result = {'results': programs}

        requests.put(
            '{}/set_config'.format(PROGRAMS_STUB_URL),
            data={'programs': json.dumps(api_result)},
        )
    def test_handle_unlinked_oauth2_user(self, mock_task):
        """Verify that the command fails when no user is associated with the OAuth2 client."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id),
                    ]),
                ]),
        ]
        self._mock_programs_api(data)

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        with self.assertRaises(CommandError):
            call_command('backpopulate_program_credentials')

        mock_task.assert_not_called()
Exemple #27
0
    def test_simulate_progress(self, mock_get_completed_courses):
        """Simulate the entirety of a user's progress through a program."""
        first_course_id, second_course_id = 'org/first-course/run', 'org/second-course/run'
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=first_course_id),
                    ]),
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=second_course_id),
                    ]),
                ]),
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[factories.RunMode()]),
                ]),
        ]
        self._mock_programs_api(data)

        # No enrollments, no program engaged.
        meter = utils.ProgramProgressMeter(self.user)
        self._assert_progress(meter)
        self.assertEqual(meter.completed_programs, [])

        # One enrollment, program engaged.
        self._create_enrollments(first_course_id)
        meter = utils.ProgramProgressMeter(self.user)
        program, program_id = data[0], data[0]['id']
        self._assert_progress(
            meter,
            factories.Progress(id=program_id,
                               in_progress=self._extract_names(program, 0),
                               not_started=self._extract_names(program, 1)))
        self.assertEqual(meter.completed_programs, [])

        # Two enrollments, program in progress.
        self._create_enrollments(second_course_id)
        meter = utils.ProgramProgressMeter(self.user)
        self._assert_progress(
            meter,
            factories.Progress(id=program_id,
                               in_progress=self._extract_names(program, 0, 1)))
        self.assertEqual(meter.completed_programs, [])

        # One valid certificate earned, one course code complete.
        mock_get_completed_courses.return_value = [
            {
                'course_id': first_course_id,
                'mode': MODES.verified
            },
        ]
        meter = utils.ProgramProgressMeter(self.user)
        self._assert_progress(
            meter,
            factories.Progress(id=program_id,
                               completed=self._extract_names(program, 0),
                               in_progress=self._extract_names(program, 1)))
        self.assertEqual(meter.completed_programs, [])

        # Invalid certificate earned, still one course code to complete.
        mock_get_completed_courses.return_value = [
            {
                'course_id': first_course_id,
                'mode': MODES.verified
            },
            {
                'course_id': second_course_id,
                'mode': MODES.honor
            },
        ]
        meter = utils.ProgramProgressMeter(self.user)
        self._assert_progress(
            meter,
            factories.Progress(id=program_id,
                               completed=self._extract_names(program, 0),
                               in_progress=self._extract_names(program, 1)))
        self.assertEqual(meter.completed_programs, [])

        # Second valid certificate obtained, all course codes complete.
        mock_get_completed_courses.return_value = [
            {
                'course_id': first_course_id,
                'mode': MODES.verified
            },
            {
                'course_id': second_course_id,
                'mode': MODES.verified
            },
        ]
        meter = utils.ProgramProgressMeter(self.user)
        self._assert_progress(
            meter,
            factories.Progress(id=program_id,
                               completed=self._extract_names(program, 0, 1)))
        self.assertEqual(meter.completed_programs, [program_id])
class BackpopulateProgramCredentialsTests(CatalogIntegrationMixin,
                                          CredentialsApiConfigMixin, TestCase):
    """Tests for the backpopulate_program_credentials management command."""
    course_id, alternate_course_id = 'org/course/run', 'org/alternate/run'

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

        self.alice = UserFactory()
        self.bob = UserFactory()

        # Disable certification to prevent the task from being triggered when
        # setting up test data (i.e., certificates with a passing status), thereby
        # skewing mock call counts.
        self.create_credentials_config(enable_learner_issuance=False)

        self.catalog_integration = self.create_catalog_integration()
        self.service_user = UserFactory(
            username=self.catalog_integration.service_username)

    @ddt.data(True, False)
    def test_handle(self, commit, mock_task, mock_get_programs):
        """Verify that relevant tasks are only enqueued when the commit option is passed."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id),
                    ]),
                ]),
        ]
        mock_get_programs.return_value = data

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.alternate_course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=commit)

        if commit:
            mock_task.assert_called_once_with(self.alice.username)
        else:
            mock_task.assert_not_called()

    @ddt.data(
        [
            factories.Program(organizations=[factories.Organization()],
                              course_codes=[
                                  factories.CourseCode(run_modes=[
                                      factories.RunMode(course_key=course_id),
                                  ]),
                              ]),
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=alternate_course_id),
                    ]),
                ]),
        ],
        [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=course_id),
                    ]),
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=alternate_course_id),
                    ]),
                ]),
        ],
        [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=course_id),
                        factories.RunMode(course_key=alternate_course_id),
                    ]),
                ]),
        ],
    )
    def test_handle_flatten(self, data, mock_task, mock_get_programs):
        """Verify that program structures are flattened correctly."""
        mock_get_programs.return_value = data

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.alternate_course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        calls = [mock.call(self.alice.username), mock.call(self.bob.username)]
        mock_task.assert_has_calls(calls, any_order=True)

    def test_handle_username_dedup(self, mock_task, mock_get_programs):
        """Verify that only one task is enqueued for a user with multiple eligible certs."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id),
                        factories.RunMode(course_key=self.alternate_course_id),
                    ]),
                ]),
        ]
        mock_get_programs.return_value = data

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.alternate_course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        mock_task.assert_called_once_with(self.alice.username)

    def test_handle_mode_slugs(self, mock_task, mock_get_programs):
        """Verify that mode slugs are taken into account."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id,
                                          mode_slug=MODES.honor),
                    ]),
                ]),
        ]
        mock_get_programs.return_value = data

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        mock_task.assert_called_once_with(self.alice.username)

    def test_handle_passing_status(self, mock_task, mock_get_programs):
        """Verify that only certificates with a passing status are selected."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id),
                        factories.RunMode(course_key=self.alternate_course_id),
                    ]),
                ]),
        ]
        mock_get_programs.return_value = data

        passing_status = CertificateStatuses.downloadable
        failing_status = CertificateStatuses.notpassing

        self.assertIn(passing_status, CertificateStatuses.PASSED_STATUSES)
        self.assertNotIn(failing_status, CertificateStatuses.PASSED_STATUSES)

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=passing_status,
        )

        # The alternate course is used here to verify that the status and run_mode
        # queries are being ANDed together correctly.
        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.alternate_course_id,
            mode=MODES.verified,
            status=failing_status,
        )

        call_command('backpopulate_program_credentials', commit=True)

        mock_task.assert_called_once_with(self.alice.username)

    def test_handle_missing_service_user(self, mock_task, __):
        """Verify that the command fails when no service user exists."""
        self.catalog_integration = self.create_catalog_integration(
            service_username='******')
        with self.assertRaises(CommandError):
            call_command('backpopulate_program_credentials')

        mock_task.assert_not_called()

    @mock.patch(COMMAND_MODULE + '.logger.exception')
    def test_handle_enqueue_failure(self, mock_log, mock_task,
                                    mock_get_programs):
        """Verify that failure to enqueue a task doesn't halt execution."""
        def side_effect(username):
            """Simulate failure to enqueue a task."""
            if username == self.alice.username:
                raise Exception

        mock_task.side_effect = side_effect

        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id),
                    ]),
                ]),
        ]
        mock_get_programs.return_value = data

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        self.assertTrue(mock_log.called)

        calls = [mock.call(self.alice.username), mock.call(self.bob.username)]
        mock_task.assert_has_calls(calls, any_order=True)
Exemple #29
0
class ProgramsDataMixin(object):
    """Mixin mocking Programs API URLs and providing fake data for testing.

    NOTE: This mixin is DEPRECATED. Tests should create and manage their own data.
    """
    PROGRAM_NAMES = [
        'Test Program A',
        'Test Program B',
        'Test Program C',
    ]

    COURSE_KEYS = [
        'organization-a/course-a/fall',
        'organization-a/course-a/winter',
        'organization-a/course-b/fall',
        'organization-a/course-b/winter',
        'organization-b/course-c/fall',
        'organization-b/course-c/winter',
        'organization-b/course-d/fall',
        'organization-b/course-d/winter',
    ]

    PROGRAMS_API_RESPONSE = {
        'results': [
            factories.Program(
                id=1,
                name=PROGRAM_NAMES[0],
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=COURSE_KEYS[0]),
                        factories.RunMode(course_key=COURSE_KEYS[1]),
                    ]),
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=COURSE_KEYS[2]),
                        factories.RunMode(course_key=COURSE_KEYS[3]),
                    ]),
                ]
            ),
            factories.Program(
                id=2,
                name=PROGRAM_NAMES[1],
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=COURSE_KEYS[4]),
                        factories.RunMode(course_key=COURSE_KEYS[5]),
                    ]),
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=COURSE_KEYS[6]),
                        factories.RunMode(course_key=COURSE_KEYS[7]),
                    ]),
                ]
            ),
            factories.Program(
                id=3,
                name=PROGRAM_NAMES[2],
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=COURSE_KEYS[7]),
                    ]),
                ]
            ),
        ]
    }

    def mock_programs_api(self, data=None, status_code=200):
        """Utility for mocking out Programs API URLs."""
        self.assertTrue(httpretty.is_enabled(), msg='httpretty must be enabled to mock Programs API calls.')

        url = ProgramsApiConfig.current().internal_api_url.strip('/') + '/programs/'

        if data is None:
            data = self.PROGRAMS_API_RESPONSE

        body = json.dumps(data)

        httpretty.reset()
        httpretty.register_uri(httpretty.GET, url, body=body, content_type='application/json', status=status_code)
class BackpopulateProgramCredentialsTests(ProgramsApiConfigMixin, TestCase):
    """Tests for the backpopulate_program_credentials management command."""
    course_id, alternate_course_id = 'org/course/run', 'org/alternate/run'

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

        self.alice = UserFactory()
        self.bob = UserFactory()
        self.oauth2_user = UserFactory()
        self.oauth2_client = ClientFactory(
            name=ProgramsApiConfig.OAUTH2_CLIENT_NAME,
            client_type=CONFIDENTIAL)

        # Disable certification to prevent the task from being triggered when
        # setting up test data (i.e., certificates with a passing status), thereby
        # skewing mock call counts.
        self.create_programs_config(enable_certification=False)

    def _link_oauth2_user(self):
        """Helper to link user and OAuth2 client."""
        self.oauth2_client.user = self.oauth2_user
        self.oauth2_client.save()  # pylint: disable=no-member

    def _mock_programs_api(self, data):
        """Helper for mocking out Programs API URLs."""
        self.assertTrue(
            httpretty.is_enabled(),
            msg='httpretty must be enabled to mock Programs API calls.')

        url = ProgramsApiConfig.current().internal_api_url.strip(
            '/') + '/programs/'
        body = json.dumps({'results': data})

        httpretty.register_uri(httpretty.GET,
                               url,
                               body=body,
                               content_type='application/json')

    @ddt.data(True, False)
    def test_handle(self, commit, mock_task):
        """Verify that relevant tasks are only enqueued when the commit option is passed."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id),
                    ]),
                ]),
        ]
        self._mock_programs_api(data)
        self._link_oauth2_user()

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.alternate_course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=commit)

        if commit:
            mock_task.assert_called_once_with(self.alice.username)
        else:
            mock_task.assert_not_called()

    @ddt.data(
        [
            factories.Program(organizations=[factories.Organization()],
                              course_codes=[
                                  factories.CourseCode(run_modes=[
                                      factories.RunMode(course_key=course_id),
                                  ]),
                              ]),
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=alternate_course_id),
                    ]),
                ]),
        ],
        [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=course_id),
                    ]),
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=alternate_course_id),
                    ]),
                ]),
        ],
        [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=course_id),
                        factories.RunMode(course_key=alternate_course_id),
                    ]),
                ]),
        ],
    )
    def test_handle_flatten(self, data, mock_task):
        """Verify that program structures are flattened correctly."""
        self._mock_programs_api(data)
        self._link_oauth2_user()

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.alternate_course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        calls = [mock.call(self.alice.username), mock.call(self.bob.username)]
        mock_task.assert_has_calls(calls, any_order=True)

    def test_handle_username_dedup(self, mock_task):
        """Verify that only one task is enqueued for a user with multiple eligible certs."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id),
                        factories.RunMode(course_key=self.alternate_course_id),
                    ]),
                ]),
        ]
        self._mock_programs_api(data)
        self._link_oauth2_user()

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.alternate_course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        mock_task.assert_called_once_with(self.alice.username)

    def test_handle_mode_slugs(self, mock_task):
        """Verify that mode slugs are taken into account."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id,
                                          mode_slug=MODES.honor),
                    ]),
                ]),
        ]
        self._mock_programs_api(data)
        self._link_oauth2_user()

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        mock_task.assert_called_once_with(self.alice.username)

    def test_handle_passing_status(self, mock_task):
        """Verify that only certificates with a passing status are selected."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id),
                        factories.RunMode(course_key=self.alternate_course_id),
                    ]),
                ]),
        ]
        self._mock_programs_api(data)
        self._link_oauth2_user()

        passing_status = CertificateStatuses.downloadable
        failing_status = CertificateStatuses.notpassing

        self.assertIn(passing_status, CertificateStatuses.PASSED_STATUSES)
        self.assertNotIn(failing_status, CertificateStatuses.PASSED_STATUSES)

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=passing_status,
        )

        # The alternate course is used here to verify that the status and run_mode
        # queries are being ANDed together correctly.
        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.alternate_course_id,
            mode=MODES.verified,
            status=failing_status,
        )

        call_command('backpopulate_program_credentials', commit=True)

        mock_task.assert_called_once_with(self.alice.username)

    def test_handle_unlinked_oauth2_user(self, mock_task):
        """Verify that the command fails when no user is associated with the OAuth2 client."""
        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id),
                    ]),
                ]),
        ]
        self._mock_programs_api(data)

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        with self.assertRaises(CommandError):
            call_command('backpopulate_program_credentials')

        mock_task.assert_not_called()

    @mock.patch(COMMAND_MODULE + '.logger.exception')
    def test_handle_enqueue_failure(self, mock_log, mock_task):
        """Verify that failure to enqueue a task doesn't halt execution."""
        def side_effect(username):
            """Simulate failure to enqueue a task."""
            if username == self.alice.username:
                raise Exception

        mock_task.side_effect = side_effect

        data = [
            factories.Program(
                organizations=[factories.Organization()],
                course_codes=[
                    factories.CourseCode(run_modes=[
                        factories.RunMode(course_key=self.course_id),
                    ]),
                ]),
        ]
        self._mock_programs_api(data)
        self._link_oauth2_user()

        GeneratedCertificateFactory(
            user=self.alice,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        GeneratedCertificateFactory(
            user=self.bob,
            course_id=self.course_id,
            mode=MODES.verified,
            status=CertificateStatuses.downloadable,
        )

        call_command('backpopulate_program_credentials', commit=True)

        self.assertTrue(mock_log.called)

        calls = [mock.call(self.alice.username), mock.call(self.bob.username)]
        mock_task.assert_has_calls(calls, any_order=True)