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_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, [])
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)
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)
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)
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)
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])
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'])
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])
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) ]
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])
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)
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)
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)
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)
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()
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)
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)