Files
edx-platform/lms/djangoapps/courseware/tests/test_about.py
Michael Terry cb1bb7fa64 test: switch default test store to the split store
It's long past time that the default test modulestore was Split,
instead of Old Mongo. This commit switches the default store and
fixes some tests that now fail:
- Tests that didn't expect MFE to be enabled (because we don't
  enable MFE for Old Mongo) - opt out of MFE for those
- Tests that hardcoded old key string formats
- Lots of other random little differences

In many places, I didn't spend much time trying to figure out how to
properly fix the test, and instead just set the modulestore to Old
Mongo.

For those tests that I didn't spend time investigating, I've set
the modulestore to TEST_DATA_MONGO_AMNESTY_MODULESTORE - search for
that string to find further work.
2022-02-04 14:32:50 -05:00

500 lines
20 KiB
Python

"""
Test the about xblock
"""
import datetime
from unittest import mock
from unittest.mock import patch
import ddt
import pytz
from ccx_keys.locator import CCXLocator
from django.conf import settings
from django.test.utils import override_settings
from django.urls import reverse
from milestones.tests.utils import MilestonesTestCaseMixin
from waffle.testutils import override_switch
from common.djangoapps.course_modes.models import CourseMode
from edx_toggles.toggles.testutils import override_waffle_flag # lint-amnesty, pylint: disable=wrong-import-order
from lms.djangoapps.ccx.tests.factories import CcxFactory
from openedx.features.course_experience import COURSE_ENABLE_UNENROLLED_ACCESS_FLAG
from openedx.features.course_experience.waffle import ENABLE_COURSE_ABOUT_SIDEBAR_HTML
from openedx.features.course_experience.waffle import WAFFLE_NAMESPACE as COURSE_EXPERIENCE_WAFFLE_NAMESPACE
from lms.djangoapps.course_home_api.toggles import COURSE_HOME_USE_LEGACY_FRONTEND
from common.djangoapps.student.tests.factories import AdminFactory, CourseEnrollmentAllowedFactory, UserFactory
from common.djangoapps.track.tests import EventTrackingTestCase
from common.djangoapps.util.milestones_helpers import get_prerequisite_courses_display, set_prerequisite_courses
from xmodule.course_module import ( # lint-amnesty, pylint: disable=wrong-import-order
CATALOG_VISIBILITY_ABOUT,
CATALOG_VISIBILITY_NONE,
COURSE_VISIBILITY_PRIVATE,
COURSE_VISIBILITY_PUBLIC,
COURSE_VISIBILITY_PUBLIC_OUTLINE
)
from xmodule.modulestore.tests.django_utils import ( # lint-amnesty, pylint: disable=wrong-import-order
TEST_DATA_MIXED_MODULESTORE,
TEST_DATA_MONGO_AMNESTY_MODULESTORE,
ModuleStoreTestCase,
SharedModuleStoreTestCase
)
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.utils import TEST_DATA_DIR # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.xml_importer import import_course_from_xml # lint-amnesty, pylint: disable=wrong-import-order
from .helpers import LoginEnrollmentTestCase
# HTML for registration button
REG_STR = "<form id=\"class_enroll_form\" method=\"post\" data-remote=\"true\" action=\"/change_enrollment\">"
SHIB_ERROR_STR = "The currently logged-in user account does not have permission to enroll in this course."
@ddt.ddt
class AboutTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase, EventTrackingTestCase, MilestonesTestCaseMixin):
"""
Tests about xblock.
"""
MODULESTORE = TEST_DATA_MONGO_AMNESTY_MODULESTORE
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course = CourseFactory.create()
cls.course_without_about = CourseFactory.create(catalog_visibility=CATALOG_VISIBILITY_NONE)
cls.course_with_about = CourseFactory.create(catalog_visibility=CATALOG_VISIBILITY_ABOUT)
cls.purchase_course = CourseFactory.create(org='MITx', number='buyme', display_name='Course To Buy')
cls.about = ItemFactory.create(
category="about", parent_location=cls.course.location,
data="OOGIE BLOOGIE", display_name="overview"
)
cls.about = ItemFactory.create(
category="about", parent_location=cls.course_without_about.location,
data="WITHOUT ABOUT", display_name="overview"
)
cls.about = ItemFactory.create(
category="about", parent_location=cls.course_with_about.location,
data="WITH ABOUT", display_name="overview"
)
def setUp(self):
super().setUp()
self.course_mode = CourseMode(
course_id=self.purchase_course.id,
mode_slug=CourseMode.DEFAULT_MODE_SLUG,
mode_display_name=CourseMode.DEFAULT_MODE_SLUG,
min_price=10
)
self.course_mode.save()
def test_anonymous_user(self):
"""
This test asserts that a non-logged in user can visit the course about page
"""
url = reverse('about_course', args=[str(self.course.id)])
resp = self.client.get(url)
self.assertContains(resp, "OOGIE BLOOGIE")
# Check that registration button is present
self.assertContains(resp, REG_STR)
def test_logged_in(self):
"""
This test asserts that a logged-in user can visit the course about page
"""
self.setup_user()
url = reverse('about_course', args=[str(self.course.id)])
resp = self.client.get(url)
self.assertContains(resp, "OOGIE BLOOGIE")
def test_already_enrolled(self):
"""
Asserts that the end user sees the appropriate messaging
when he/she visits the course about page, but is already enrolled
"""
self.setup_user()
self.enroll(self.course, True)
url = reverse('about_course', args=[str(self.course.id)])
resp = self.client.get(url)
self.assertContains(resp, "You are enrolled in this course")
self.assertContains(resp, "View Course")
@override_settings(COURSE_ABOUT_VISIBILITY_PERMISSION="see_about_page")
def test_visible_about_page_settings(self):
"""
Verify that the About Page honors the permission settings in the course module
"""
url = reverse('about_course', args=[str(self.course_with_about.id)])
resp = self.client.get(url)
self.assertContains(resp, "WITH ABOUT")
url = reverse('about_course', args=[str(self.course_without_about.id)])
resp = self.client.get(url)
assert resp.status_code == 404
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
def test_logged_in_marketing(self):
self.setup_user()
url = reverse('about_course', args=[str(self.course.id)])
resp = self.client.get(url)
# should be redirected
assert resp.status_code == 302
# follow this time, and check we're redirected to the course home page
resp = self.client.get(url, follow=True)
target_url = resp.redirect_chain[-1][0]
course_home_url = reverse('openedx.course_experience.course_home', args=[str(self.course.id)])
assert target_url.endswith(course_home_url)
@patch.dict(settings.FEATURES, {'ENABLE_COURSE_HOME_REDIRECT': False})
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': True})
def test_logged_in_marketing_without_course_home_redirect(self):
"""
Verify user is not redirected to course home page when
ENABLE_COURSE_HOME_REDIRECT is set to False
"""
self.setup_user()
url = reverse('about_course', args=[str(self.course.id)])
resp = self.client.get(url)
# should not be redirected
self.assertContains(resp, "OOGIE BLOOGIE")
@patch.dict(settings.FEATURES, {'ENABLE_COURSE_HOME_REDIRECT': True})
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_SITE': False})
def test_logged_in_marketing_without_mktg_site(self):
"""
Verify user is not redirected to course home page when
ENABLE_MKTG_SITE is set to False
"""
self.setup_user()
url = reverse('about_course', args=[str(self.course.id)])
resp = self.client.get(url)
# should not be redirected
self.assertContains(resp, "OOGIE BLOOGIE")
@patch.dict(settings.FEATURES, {'ENABLE_PREREQUISITE_COURSES': True})
def test_pre_requisite_course(self):
pre_requisite_course = CourseFactory.create(org='edX', course='900', display_name='pre requisite course')
course = CourseFactory.create(pre_requisite_courses=[str(pre_requisite_course.id)])
self.setup_user()
url = reverse('about_course', args=[str(course.id)])
resp = self.client.get(url)
assert resp.status_code == 200
pre_requisite_courses = get_prerequisite_courses_display(course)
pre_requisite_course_about_url = reverse('about_course', args=[str(pre_requisite_courses[0]['key'])])
assert '<span class="important-dates-item-text pre-requisite"><a href="{}">{}</a></span>'.format(pre_requisite_course_about_url, pre_requisite_courses[0]['display']) in resp.content.decode(resp.charset).strip('\n') # pylint: disable=line-too-long
@patch.dict(settings.FEATURES, {'ENABLE_PREREQUISITE_COURSES': True})
def test_about_page_unfulfilled_prereqs(self):
pre_requisite_course = CourseFactory.create(
org='edX',
course='901',
display_name='pre requisite course',
)
pre_requisite_courses = [str(pre_requisite_course.id)]
# for this failure to occur, the enrollment window needs to be in the past
course = CourseFactory.create(
org='edX',
course='1000',
# closed enrollment
enrollment_start=datetime.datetime(2013, 1, 1),
enrollment_end=datetime.datetime(2014, 1, 1),
start=datetime.datetime(2013, 1, 1),
end=datetime.datetime(2030, 1, 1),
pre_requisite_courses=pre_requisite_courses,
)
set_prerequisite_courses(course.id, pre_requisite_courses)
self.setup_user()
self.enroll(self.course, True)
self.enroll(pre_requisite_course, True)
url = reverse('about_course', args=[str(course.id)])
resp = self.client.get(url)
assert resp.status_code == 200
pre_requisite_courses = get_prerequisite_courses_display(course)
pre_requisite_course_about_url = reverse('about_course', args=[str(pre_requisite_courses[0]['key'])])
assert '<span class="important-dates-item-text pre-requisite"><a href="{}">{}</a></span>'.format(pre_requisite_course_about_url, pre_requisite_courses[0]['display']) in resp.content.decode(resp.charset).strip('\n') # pylint: disable=line-too-long
url = reverse('about_course', args=[str(pre_requisite_course.id)])
resp = self.client.get(url)
assert resp.status_code == 200
@ddt.data(
[COURSE_VISIBILITY_PRIVATE],
[COURSE_VISIBILITY_PUBLIC_OUTLINE],
[COURSE_VISIBILITY_PUBLIC],
)
@ddt.unpack
def test_about_page_public_view(self, course_visibility):
"""
Assert that anonymous or unenrolled users see View Course option
when unenrolled access flag is set
"""
with mock.patch('xmodule.course_module.CourseBlock.course_visibility', course_visibility):
with override_waffle_flag(COURSE_ENABLE_UNENROLLED_ACCESS_FLAG, active=True):
url = reverse('about_course', args=[str(self.course.id)])
resp = self.client.get(url)
if course_visibility == COURSE_VISIBILITY_PUBLIC or course_visibility == COURSE_VISIBILITY_PUBLIC_OUTLINE: # lint-amnesty, pylint: disable=consider-using-in
self.assertContains(resp, "View Course")
else:
self.assertContains(resp, "Enroll Now")
class AboutTestCaseXML(LoginEnrollmentTestCase, ModuleStoreTestCase):
"""
Tests for the course about page
"""
MODULESTORE = TEST_DATA_MIXED_MODULESTORE
def setUp(self):
"""
Set up the tests
"""
super().setUp()
# The following test course (which lives at common/test/data/2014)
# is closed; we're testing that an about page still appears when
# the course is already closed
self.xml_course_id = self.store.make_course_key('edX', 'detached_pages', '2014')
import_course_from_xml(
self.store,
'test_user',
TEST_DATA_DIR,
source_dirs=['2014'],
static_content_store=None,
target_id=self.xml_course_id,
raise_on_failure=True,
create_if_not_present=True,
)
# this text appears in that course's about page
# common/test/data/2014/about/overview.html
self.xml_data = "about page 463139"
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_logged_in_xml(self):
self.setup_user()
url = reverse('about_course', args=[str(self.xml_course_id)])
resp = self.client.get(url)
self.assertContains(resp, self.xml_data)
@patch.dict('django.conf.settings.FEATURES', {'DISABLE_START_DATES': False})
def test_anonymous_user_xml(self):
url = reverse('about_course', args=[str(self.xml_course_id)])
resp = self.client.get(url)
self.assertContains(resp, self.xml_data)
class AboutWithCappedEnrollmentsTestCase(LoginEnrollmentTestCase, SharedModuleStoreTestCase):
"""
This test case will check the About page when a course has a capped enrollment
"""
MODULESTORE = TEST_DATA_MONGO_AMNESTY_MODULESTORE
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course = CourseFactory.create(metadata={"max_student_enrollments_allowed": 1})
cls.about = ItemFactory.create(
category="about", parent_location=cls.course.location,
data="OOGIE BLOOGIE", display_name="overview"
)
def test_enrollment_cap(self):
"""
This test will make sure that enrollment caps are enforced
"""
self.setup_user()
url = reverse('about_course', args=[str(self.course.id)])
resp = self.client.get(url)
self.assertContains(resp, '<a href="#" class="register">')
self.enroll(self.course, verify=True)
# pylint: disable=attribute-defined-outside-init
# create a new account since the first account is already enrolled in the course
self.email = 'foo_second@test.com'
self.password = 'bar'
self.username = 'test_second'
self.create_account(self.username, self.email, self.password)
self.activate_user(self.email)
self.login(self.email, self.password)
# Get the about page again and make sure that the page says that the course is full
resp = self.client.get(url)
self.assertContains(resp, "Course is full")
# Try to enroll as well
result = self.enroll(self.course)
assert not result
# Check that registration button is not present
self.assertNotContains(resp, REG_STR)
class AboutWithInvitationOnly(SharedModuleStoreTestCase):
"""
This test case will check the About page when a course is invitation only.
"""
MODULESTORE = TEST_DATA_MONGO_AMNESTY_MODULESTORE
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course = CourseFactory.create(metadata={"invitation_only": True})
cls.about = ItemFactory.create(
category="about", parent_location=cls.course.location,
display_name="overview"
)
def test_invitation_only(self):
"""
Test for user not logged in, invitation only course.
"""
url = reverse('about_course', args=[str(self.course.id)])
resp = self.client.get(url)
self.assertContains(resp, "Enrollment in this course is by invitation only")
# Check that registration button is not present
self.assertNotContains(resp, REG_STR)
def test_invitation_only_but_allowed(self):
"""
Test for user logged in and allowed to enroll in invitation only course.
"""
# Course is invitation only, student is allowed to enroll and logged in
user = UserFactory.create(username='allowed_student', password='test', email='allowed_student@test.com')
CourseEnrollmentAllowedFactory(email=user.email, course_id=self.course.id)
self.client.login(username=user.username, password='test')
url = reverse('about_course', args=[str(self.course.id)])
resp = self.client.get(url)
self.assertContains(resp, "Enroll Now")
# Check that registration button is present
self.assertContains(resp, REG_STR)
class AboutWithClosedEnrollment(ModuleStoreTestCase):
"""
This test case will check the About page for a course that has enrollment start/end
set but it is currently outside of that period.
"""
MODULESTORE = TEST_DATA_MONGO_AMNESTY_MODULESTORE
def setUp(self):
super().setUp()
self.course = CourseFactory.create(metadata={"invitation_only": False})
# Setup enrollment period to be in future
now = datetime.datetime.now(pytz.UTC)
tomorrow = now + datetime.timedelta(days=1)
nextday = tomorrow + datetime.timedelta(days=1)
self.course.enrollment_start = tomorrow
self.course.enrollment_end = nextday
self.course = self.update_course(self.course, self.user.id)
self.about = ItemFactory.create(
category="about", parent_location=self.course.location,
display_name="overview"
)
def test_closed_enrollmement(self):
url = reverse('about_course', args=[str(self.course.id)])
resp = self.client.get(url)
self.assertContains(resp, "Enrollment is Closed")
# Check that registration button is not present
self.assertNotContains(resp, REG_STR)
def test_course_price_is_not_visble_in_sidebar(self):
url = reverse('about_course', args=[str(self.course.id)])
resp = self.client.get(url)
# course price is not visible ihe course_about page when the course
# mode is not set to honor
self.assertNotContains(resp, '<span class="important-dates-item-text">$10</span>')
@ddt.ddt
class AboutSidebarHTMLTestCase(SharedModuleStoreTestCase):
"""
This test case will check the About page for the content in the HTML sidebar.
"""
def setUp(self):
super().setUp()
self.course = CourseFactory.create()
@ddt.data(
("", "", False),
("about_sidebar_html", "About Sidebar HTML Heading", False),
("about_sidebar_html", "", False),
("", "", True),
("about_sidebar_html", "About Sidebar HTML Heading", True),
("about_sidebar_html", "", True),
)
@ddt.unpack
def test_html_sidebar_enabled(self, itemfactory_display_name, itemfactory_data, waffle_switch_value):
with override_switch(
'{}.{}'.format(
COURSE_EXPERIENCE_WAFFLE_NAMESPACE,
ENABLE_COURSE_ABOUT_SIDEBAR_HTML
),
active=waffle_switch_value
):
if itemfactory_display_name:
ItemFactory.create(
category="about",
parent_location=self.course.location,
display_name=itemfactory_display_name,
data=itemfactory_data,
)
url = reverse('about_course', args=[str(self.course.id)])
resp = self.client.get(url)
if waffle_switch_value and itemfactory_display_name and itemfactory_data:
self.assertContains(resp, '<section class="about-sidebar-html">')
self.assertContains(resp, itemfactory_data)
else:
self.assertNotContains(resp, '<section class="about-sidebar-html">')
class CourseAboutTestCaseCCX(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
"""
Test for unenrolled student tries to access ccx.
Note: Only CCX coach can enroll a student in CCX. In sum self-registration not allowed.
"""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.course = CourseFactory.create()
def setUp(self):
super().setUp()
# Create ccx coach account
self.coach = coach = AdminFactory.create(password="test")
self.client.login(username=coach.username, password="test")
@override_waffle_flag(COURSE_HOME_USE_LEGACY_FRONTEND, active=True)
def test_redirect_to_dashboard_unenrolled_ccx(self):
"""
Assert that when unenrolled user tries to access CCX do not allow the user to self-register.
Redirect them to their student dashboard
"""
# create ccx
ccx = CcxFactory(course_id=self.course.id, coach=self.coach)
ccx_locator = CCXLocator.from_course_locator(self.course.id, str(ccx.id))
self.setup_user()
url = reverse('openedx.course_experience.course_home', args=[ccx_locator])
response = self.client.get(url)
expected = reverse('dashboard')
self.assertRedirects(response, expected, status_code=302, target_status_code=200)