def test_retry_failed_edx_enrollments(mocker, exception_raised): """ Tests that retry_failed_edx_enrollments loops through enrollments that failed in edX and attempts to enroll them again """ with freeze_time(now_in_utc() - timedelta(days=1)): failed_enrollments = CourseRunEnrollmentFactory.create_batch( 3, edx_enrolled=False, user__is_active=True) CourseRunEnrollmentFactory.create(edx_enrolled=False, user__is_active=False) patched_enroll_in_edx = mocker.patch( "courseware.api.enroll_in_edx_course_runs", side_effect=[None, exception_raised or None, None], ) patched_log_exception = mocker.patch("courseware.api.log.exception") successful_enrollments = retry_failed_edx_enrollments() assert patched_enroll_in_edx.call_count == len(failed_enrollments) assert len(successful_enrollments) == (3 if exception_raised is None else 2) assert patched_log_exception.called == bool(exception_raised) if exception_raised: failed_enroll_user, failed_enroll_runs = patched_enroll_in_edx.call_args_list[ 1][0] expected_successful_enrollments = [ e for e in failed_enrollments if e.user != failed_enroll_user and e.run != failed_enroll_runs[0] ] assert {e.id for e in successful_enrollments } == {e.id for e in expected_successful_enrollments}
def test_create_run_enrollments(mocker, user): """ create_run_enrollments should call the edX API to create enrollments, create or reactivate local enrollment records, and notify enrolled users via email """ num_runs = 3 order = OrderFactory.create() company = CompanyFactory.create() runs = CourseRunFactory.create_batch(num_runs) # Create an existing deactivate enrollment to test that it gets reactivated CourseRunEnrollmentFactory.create( user=user, run=runs[0], order=order, change_status=ENROLL_CHANGE_STATUS_REFUNDED, active=False, ) patched_edx_enroll = mocker.patch("courses.api.enroll_in_edx_course_runs") patched_send_enrollment_email = mocker.patch( "courses.api.mail_api.send_course_run_enrollment_email") successful_enrollments, edx_request_success = create_run_enrollments( user, runs, order=order, company=company) patched_edx_enroll.assert_called_once_with(user, runs) assert patched_send_enrollment_email.call_count == num_runs assert edx_request_success is True assert len(successful_enrollments) == num_runs enrollments = CourseRunEnrollment.objects.order_by("run__id").all() for (run, enrollment) in zip(runs, enrollments): assert enrollment.change_status is None assert enrollment.active is True assert enrollment.edx_enrolled is True assert enrollment.run == run patched_send_enrollment_email.assert_any_call(enrollment)
def test_course_available_runs(): """enrolled runs for a user should not be in the list of available runs""" user = UserFactory.create() course = CourseFactory.create() runs = CourseRunFactory.create_batch(2, course=course, live=True) runs.sort(key=lambda run: run.start_date) CourseRunEnrollmentFactory.create(run=runs[0], user=user) assert course.available_runs(user) == [runs[1]] assert course.available_runs(UserFactory.create()) == runs
def test_course_view(client, user, home_page, is_enrolled, has_unexpired_run, has_product, is_anonymous): """ Test that the course detail view has the right context and shows the right HTML for the enroll/view button """ course = CourseFactory.create(live=True, page__parent=home_page) if has_unexpired_run: run = CourseRunFactory.create(course=course, live=True) else: run = None if has_product and has_unexpired_run: product_id = ProductVersionFactory.create(product=ProductFactory( content_object=run)).product.id else: product_id = None if is_enrolled and has_unexpired_run: CourseRunEnrollmentFactory.create(user=user, run=run) if not is_anonymous: client.force_login(user) resp = client.get(course.page.get_url()) assert resp.context["user"] == user if not is_anonymous else AnonymousUser( ) assert resp.context["product_id"] == product_id assert resp.context["enrolled"] == (is_enrolled and has_unexpired_run and not is_anonymous) # Anynoymous users don't see the enrolled/enroll-now button. # For logged in users: # a) product should exist, next courserun should be there, user not enrolled (enroll now button) # b) user is enrolled (enrolled button) # NOTE: added `has_unexpired_run` to test for case (b) only because of the way the test is written, # enrollment isn't actually created unless the course has an unexpired run. has_button = ((has_product and has_unexpired_run and not is_enrolled) or (is_enrolled and has_unexpired_run)) and not is_anonymous url = "" # make linter happy class_name = "" if not is_anonymous: if not is_enrolled and has_product and has_unexpired_run: url = f'{reverse("checkout-page")}?product={product_id}' class_name = "enroll-now" if is_enrolled and has_unexpired_run: url = reverse("user-dashboard") class_name = "enrolled" assert ( f'<a class="enroll-button {class_name}" href="{url}">'.encode("utf-8") in resp.content) is has_button assert ("Please Sign In to MITx PRO to enroll in a course".encode("utf-8") in resp.content) is (is_anonymous and has_product and has_unexpired_run)
def test_unenroll_edx_course_run_failure(mocker, client_exception_raised, expected_exception): """Tests that unenroll_edx_course_run translates exceptions raised by the API client""" run_enrollment = CourseRunEnrollmentFactory.create(edx_enrolled=True) mock_client = mocker.MagicMock() mock_client.enrollments.deactivate_enrollment = mocker.Mock( side_effect=client_exception_raised) mocker.patch("courseware.api.get_edx_api_client", return_value=mock_client) with pytest.raises(expected_exception): unenroll_edx_course_run(run_enrollment)
def test_send_course_run_enrollment_email_error(mocker): """send_course_run_enrollment_email handle and log errors""" patched_mail_api = mocker.patch("ecommerce.mail_api.api") patched_log = mocker.patch("ecommerce.mail_api.log") patched_mail_api.send_message.side_effect = Exception("error") enrollment = CourseRunEnrollmentFactory.create() send_course_run_enrollment_email(enrollment) patched_log.exception.assert_called_once_with( "Error sending enrollment success email")
def test_retry_failed_enroll_grace_period(mocker): """ Tests that retry_failed_edx_enrollments does not attempt to repair any enrollments that were recently created """ now = now_in_utc() with freeze_time(now - timedelta( minutes=COURSEWARE_REPAIR_GRACE_PERIOD_MINS - 1)): CourseRunEnrollmentFactory.create(edx_enrolled=False, user__is_active=True) with freeze_time(now - timedelta( minutes=COURSEWARE_REPAIR_GRACE_PERIOD_MINS + 1)): older_enrollment = CourseRunEnrollmentFactory.create( edx_enrolled=False, user__is_active=True) patched_enroll_in_edx = mocker.patch( "courseware.api.enroll_in_edx_course_runs") successful_enrollments = retry_failed_edx_enrollments() assert successful_enrollments == [older_enrollment] patched_enroll_in_edx.assert_called_once_with(older_enrollment.user, [older_enrollment.run])
def test_deactivate_and_save(): """Test that the deactivate_and_save method in enrollment models sets properties and saves""" course_run_enrollment = CourseRunEnrollmentFactory.create( active=True, change_status=None) program_enrollment = ProgramEnrollmentFactory.create(active=True, change_status=None) enrollments = [course_run_enrollment, program_enrollment] for enrollment in enrollments: enrollment.deactivate_and_save(ENROLL_CHANGE_STATUS_REFUNDED) enrollment.refresh_from_db() enrollment.active = False enrollment.change_status = ENROLL_CHANGE_STATUS_REFUNDED
def test_unenroll_edx_course_run(mocker): """Tests that unenroll_edx_course_run makes a call to unenroll in edX via the API client""" mock_client = mocker.MagicMock() run_enrollment = CourseRunEnrollmentFactory.create(edx_enrolled=True) courseware_id = run_enrollment.run.courseware_id enroll_return_value = mocker.Mock(json={"course_id": courseware_id}) mock_client.enrollments.deactivate_enrollment = mocker.Mock( return_value=enroll_return_value) mocker.patch("courseware.api.get_edx_api_client", return_value=mock_client) deactivated_enrollment = unenroll_edx_course_run(run_enrollment) mock_client.enrollments.deactivate_enrollment.assert_called_once_with( courseware_id) assert deactivated_enrollment == enroll_return_value
def test_defer_enrollment(mocker, keep_failed_enrollments): """ defer_enrollment should deactivate a user's existing enrollment and create an enrollment in another course run """ course = CourseFactory.create() course_runs = CourseRunFactory.create_batch(3, course=course) order = OrderFactory.create() company = CompanyFactory.create() existing_enrollment = CourseRunEnrollmentFactory.create(run=course_runs[0], order=order, company=company) target_run = course_runs[1] mock_new_enrollment = mocker.Mock() patched_create_enrollments = mocker.patch( "courses.api.create_run_enrollments", autospec=True, return_value=([ mock_new_enrollment if keep_failed_enrollments else None ], True), ) patched_deactivate_enrollments = mocker.patch( "courses.api.deactivate_run_enrollment", autospec=True, return_value=existing_enrollment if keep_failed_enrollments else None, ) returned_from_enrollment, returned_to_enrollment = defer_enrollment( existing_enrollment.user, existing_enrollment.run.courseware_id, course_runs[1].courseware_id, keep_failed_enrollments=keep_failed_enrollments, ) assert returned_from_enrollment == patched_deactivate_enrollments.return_value assert returned_to_enrollment == patched_create_enrollments.return_value[ 0][0] patched_create_enrollments.assert_called_once_with( existing_enrollment.user, [target_run], order=order, company=company, keep_failed_enrollments=keep_failed_enrollments, ) patched_deactivate_enrollments.assert_called_once_with( existing_enrollment, ENROLL_CHANGE_STATUS_DEFERRED, keep_failed_enrollments=keep_failed_enrollments, )
def test_send_course_run_enrollment_email(mocker): """send_course_run_enrollment_email should send an email for the given enrollment""" patched_mail_api = mocker.patch("ecommerce.mail_api.api") enrollment = CourseRunEnrollmentFactory.create() send_course_run_enrollment_email(enrollment) patched_mail_api.context_for_user.assert_called_once_with( user=enrollment.user, extra_context={"enrollment": enrollment}) patched_mail_api.message_for_recipient.assert_called_once_with( enrollment.user.email, patched_mail_api.context_for_user.return_value, EMAIL_COURSE_RUN_ENROLLMENT, ) patched_mail_api.send_message.assert_called_once_with( patched_mail_api.message_for_recipient.return_value)
def test_deactivate_run_enrollment(self, patches): """ deactivate_run_enrollment should attempt to unenroll a user in a course run in edX and set the local enrollment record to inactive """ enrollment = CourseRunEnrollmentFactory.create(edx_enrolled=True) returned_enrollment = deactivate_run_enrollment( enrollment, change_status=ENROLL_CHANGE_STATUS_REFUNDED) patches.edx_unenroll.assert_called_once_with(enrollment) patches.send_unenrollment_email.assert_called_once_with(enrollment) enrollment.refresh_from_db() assert enrollment.change_status == ENROLL_CHANGE_STATUS_REFUNDED assert enrollment.active is False assert enrollment.edx_enrolled is False assert returned_enrollment == enrollment
def test_audit(user, is_program, has_company): """Test audit table serialization""" enrollment = (ProgramEnrollmentFactory.create() if is_program else CourseRunEnrollmentFactory.create()) if has_company: enrollment.company = CompanyFactory.create() enrollment.save_and_log(user) expected = { "active": enrollment.active, "change_status": enrollment.change_status, "created_on": format_as_iso8601(enrollment.created_on), "company": enrollment.company.id if has_company else None, "company_name": enrollment.company.name if has_company else None, "email": enrollment.user.email, "full_name": enrollment.user.name, "id": enrollment.id, "order": enrollment.order.id, "text_id": enrollment.program.readable_id if is_program else enrollment.run.courseware_id, "updated_on": format_as_iso8601(enrollment.updated_on), "user": enrollment.user.id, "username": enrollment.user.username, } if not is_program: expected["edx_enrolled"] = enrollment.edx_enrolled expected["run"] = enrollment.run.id else: expected["program"] = enrollment.program.id assert (enrollment.get_audit_class().objects.get( enrollment=enrollment).data_after == expected)
def test_deactivate_run_enrollment_api_fail(self, patches, keep_failed_enrollments): """ If a flag is provided, deactivate_run_enrollment should set local enrollment record to inactive even if the API call fails """ enrollment = CourseRunEnrollmentFactory.create(edx_enrolled=True) patches.edx_unenroll.side_effect = Exception deactivate_run_enrollment( enrollment, change_status=ENROLL_CHANGE_STATUS_REFUNDED, keep_failed_enrollments=keep_failed_enrollments, ) patches.edx_unenroll.assert_called_once_with(enrollment) patches.send_unenrollment_email.assert_not_called() patches.log_exception.assert_called_once() enrollment.refresh_from_db() assert enrollment.active is not keep_failed_enrollments
def test_enrollment_is_ended(): """Verify that is_ended returns True, if all of course runs in a program/course are ended.""" past_date = now_in_utc() - timedelta(days=1) past_program = ProgramFactory.create() past_course = CourseFactory.create() past_course_runs = CourseRunFactory.create_batch( 3, end_date=past_date, course=past_course, course__program=past_program) program_enrollment = ProgramEnrollmentFactory.create(program=past_program) course_enrollment = CourseRunEnrollmentFactory.create( run=past_course_runs[0]) assert program_enrollment.is_ended assert course_enrollment.is_ended
def test_serialize_course_run_enrollments(settings, has_company, receipts_enabled): """Test that CourseRunEnrollmentSerializer has correct data""" settings.ENABLE_ORDER_RECEIPTS = receipts_enabled course_run_enrollment = CourseRunEnrollmentFactory.create( has_company_affiliation=has_company) serialized_data = CourseRunEnrollmentSerializer(course_run_enrollment).data assert serialized_data == { "run": CourseRunDetailSerializer(course_run_enrollment.run).data, "company": (CompanySerializer(course_run_enrollment.company).data if has_company else None), "certificate": None, "receipt": course_run_enrollment.order_id if course_run_enrollment.order.status == Order.FULFILLED and receipts_enabled else None, }
def test_get_user_enrollments(user): """Test that get_user_enrollments returns an object with a user's program and course enrollments""" past_date = now_in_utc() - timedelta(days=1) past_start_dates = [ now_in_utc() - timedelta(days=2), now_in_utc() - timedelta(days=3), now_in_utc() - timedelta(days=4), ] program = ProgramFactory.create() past_program = ProgramFactory.create() program_course_runs = CourseRunFactory.create_batch( 3, course__program=program) past_program_course_runs = CourseRunFactory.create_batch( 3, start_date=factory.Iterator(past_start_dates), end_date=past_date, course__program=past_program, ) non_program_course_runs = CourseRunFactory.create_batch( 2, course__program=None) past_non_program_course_runs = CourseRunFactory.create_batch( 2, start_date=factory.Iterator(past_start_dates), end_date=past_date, course__program=None, ) all_course_runs = (program_course_runs + past_program_course_runs + non_program_course_runs + past_non_program_course_runs) course_run_enrollments = CourseRunEnrollmentFactory.create_batch( len(all_course_runs), run=factory.Iterator(all_course_runs), user=user) program_enrollment = ProgramEnrollmentFactory.create(program=program, user=user) past_program_enrollment = ProgramEnrollmentFactory.create( program=past_program, user=user) # Add a non-active enrollment so we can confirm that it isn't returned CourseRunEnrollmentFactory.create(user=user, active=False) def key_func(enrollment): """ Function for sorting runs by start_date""" return enrollment.run.start_date user_enrollments = get_user_enrollments(user) assert list(user_enrollments.programs) == [program_enrollment] assert list(user_enrollments.past_programs) == [past_program_enrollment] assert list(user_enrollments.program_runs) == sorted( [ run_enrollment for run_enrollment in course_run_enrollments if run_enrollment.run in program_course_runs + past_program_course_runs ], key=key_func, ) assert list(user_enrollments.non_program_runs) == sorted( [ run_enrollment for run_enrollment in course_run_enrollments if run_enrollment.run in non_program_course_runs ], key=key_func, ) assert list(user_enrollments.past_non_program_runs) == sorted( [ run_enrollment for run_enrollment in course_run_enrollments if run_enrollment.run in past_non_program_course_runs ], key=key_func, )
def test_serialize_course(mock_context, is_anonymous, all_runs): """Test Course serialization""" now = datetime.now(tz=pytz.UTC) if is_anonymous: mock_context["request"].user = AnonymousUser() if all_runs: mock_context["all_runs"] = True user = mock_context["request"].user course_run = CourseRunFactory.create(course__no_program=True, live=True) course = course_run.course topic = "a course topic" course.topics.set([CourseTopic.objects.create(name=topic)]) # Create expired, enrollment_ended, future, and enrolled course runs CourseRunFactory.create(course=course, end_date=now - timedelta(1), live=True) CourseRunFactory.create(course=course, enrollment_end=now - timedelta(1), live=True) CourseRunFactory.create(course=course, enrollment_start=now + timedelta(1), live=True) enrolled_run = CourseRunFactory.create(course=course, live=True) unexpired_runs = [enrolled_run, course_run] CourseRunEnrollmentFactory.create(run=enrolled_run, **({} if is_anonymous else { "user": user })) # create products for all courses so the serializer shows them for run in CourseRun.objects.all(): ProductVersionFactory.create(product__content_object=run) data = CourseSerializer(instance=course, context=mock_context).data if all_runs or is_anonymous: expected_runs = unexpired_runs else: expected_runs = [course_run] assert_drf_json_equal( data, { "title": course.title, "description": course.page.description, "readable_id": course.readable_id, "id": course.id, "courseruns": [ CourseRunSerializer(run).data for run in sorted(expected_runs, key=lambda run: run.start_date) ], "thumbnail_url": f"http://localhost:8053{course.page.thumbnail_image.file.url}", "next_run_id": course.first_unexpired_run.id, "topics": [{ "name": topic }], }, )