367 lines
12 KiB
Python
367 lines
12 KiB
Python
"""
|
|
Provide tests for sysadmin dashboard feature in sysadmin.py
|
|
"""
|
|
import glob
|
|
import os
|
|
import re
|
|
import shutil
|
|
import unittest
|
|
from uuid import uuid4
|
|
from util.date_utils import get_time_display, DEFAULT_DATE_TIME_FORMAT
|
|
from nose.plugins.attrib import attr
|
|
|
|
from django.conf import settings
|
|
from django.core.urlresolvers import reverse
|
|
from django.test.client import Client
|
|
from django.test.utils import override_settings
|
|
from django.utils.timezone import utc as UTC
|
|
import mongoengine
|
|
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
|
|
|
from dashboard.models import CourseImportLog
|
|
from dashboard.git_import import GitImportErrorNoDir
|
|
from datetime import datetime
|
|
from student.roles import CourseStaffRole, GlobalStaff
|
|
from student.tests.factories import UserFactory
|
|
from xmodule.modulestore.django import modulestore
|
|
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
|
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
|
|
|
|
|
|
TEST_MONGODB_LOG = {
|
|
'host': MONGO_HOST,
|
|
'port': MONGO_PORT_NUM,
|
|
'user': '',
|
|
'password': '',
|
|
'db': 'test_xlog',
|
|
}
|
|
|
|
FEATURES_WITH_SSL_AUTH = settings.FEATURES.copy()
|
|
FEATURES_WITH_SSL_AUTH['AUTH_USE_CERTIFICATES'] = True
|
|
|
|
|
|
class SysadminBaseTestCase(SharedModuleStoreTestCase):
|
|
"""
|
|
Base class with common methods used in XML and Mongo tests
|
|
"""
|
|
|
|
TEST_REPO = 'https://github.com/mitocw/edx4edx_lite.git'
|
|
TEST_BRANCH = 'testing_do_not_delete'
|
|
TEST_BRANCH_COURSE = SlashSeparatedCourseKey('MITx', 'edx4edx_branch', 'edx4edx')
|
|
|
|
def setUp(self):
|
|
"""Setup test case by adding primary user."""
|
|
super(SysadminBaseTestCase, self).setUp()
|
|
self.user = UserFactory.create(username='test_user',
|
|
email='test_user+sysadmin@edx.org',
|
|
password='foo')
|
|
self.client = Client()
|
|
|
|
def _setstaff_login(self):
|
|
"""Makes the test user staff and logs them in"""
|
|
GlobalStaff().add_users(self.user)
|
|
self.client.login(username=self.user.username, password='foo')
|
|
|
|
def _add_edx4edx(self, branch=None):
|
|
"""Adds the edx4edx sample course"""
|
|
post_dict = {'repo_location': self.TEST_REPO, 'action': 'add_course', }
|
|
if branch:
|
|
post_dict['repo_branch'] = branch
|
|
return self.client.post(reverse('sysadmin_courses'), post_dict)
|
|
|
|
def _rm_edx4edx(self):
|
|
"""Deletes the sample course from the XML store"""
|
|
def_ms = modulestore()
|
|
course_path = '{0}/edx4edx_lite'.format(
|
|
os.path.abspath(settings.DATA_DIR))
|
|
try:
|
|
# using XML store
|
|
course = def_ms.courses.get(course_path, None)
|
|
except AttributeError:
|
|
# Using mongo store
|
|
course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
|
|
|
|
# Delete git loaded course
|
|
response = self.client.post(
|
|
reverse('sysadmin_courses'),
|
|
{
|
|
'course_id': course.id.to_deprecated_string(),
|
|
'action': 'del_course',
|
|
}
|
|
)
|
|
self.addCleanup(self._rm_glob, '{0}_deleted_*'.format(course_path))
|
|
|
|
return response
|
|
|
|
def _rm_glob(self, path):
|
|
"""
|
|
Create a shell expansion of passed in parameter and iteratively
|
|
remove them. Must only expand to directories.
|
|
"""
|
|
for path in glob.glob(path):
|
|
shutil.rmtree(path)
|
|
|
|
def _mkdir(self, path):
|
|
"""
|
|
Create directory and add the cleanup for it.
|
|
"""
|
|
os.mkdir(path)
|
|
self.addCleanup(shutil.rmtree, path)
|
|
|
|
|
|
@attr('shard_1')
|
|
@override_settings(
|
|
MONGODB_LOG=TEST_MONGODB_LOG,
|
|
GIT_REPO_DIR=settings.TEST_ROOT / "course_repos_{}".format(uuid4().hex)
|
|
)
|
|
@unittest.skipUnless(settings.FEATURES.get('ENABLE_SYSADMIN_DASHBOARD'),
|
|
"ENABLE_SYSADMIN_DASHBOARD not set")
|
|
class TestSysAdminMongoCourseImport(SysadminBaseTestCase):
|
|
"""
|
|
Check that importing into the mongo module store works
|
|
"""
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
"""Delete mongo log entries after test."""
|
|
super(TestSysAdminMongoCourseImport, cls).tearDownClass()
|
|
try:
|
|
mongoengine.connect(TEST_MONGODB_LOG['db'])
|
|
CourseImportLog.objects.all().delete()
|
|
except mongoengine.connection.ConnectionError:
|
|
pass
|
|
|
|
def _setstaff_login(self):
|
|
"""
|
|
Makes the test user staff and logs them in
|
|
"""
|
|
|
|
self.user.is_staff = True
|
|
self.user.save()
|
|
|
|
self.client.login(username=self.user.username, password='foo')
|
|
|
|
def test_missing_repo_dir(self):
|
|
"""
|
|
Ensure that we handle a missing repo dir
|
|
"""
|
|
|
|
self._setstaff_login()
|
|
|
|
if os.path.isdir(settings.GIT_REPO_DIR):
|
|
shutil.rmtree(settings.GIT_REPO_DIR)
|
|
|
|
# Create git loaded course
|
|
response = self._add_edx4edx()
|
|
self.assertIn(GitImportErrorNoDir(settings.GIT_REPO_DIR).message,
|
|
response.content.decode('UTF-8'))
|
|
|
|
def test_mongo_course_add_delete(self):
|
|
"""
|
|
This is the same as TestSysadmin.test_xml_course_add_delete,
|
|
but it uses a mongo store
|
|
"""
|
|
|
|
self._setstaff_login()
|
|
self._mkdir(settings.GIT_REPO_DIR)
|
|
|
|
def_ms = modulestore()
|
|
self.assertFalse('xml' == def_ms.get_modulestore_type(None))
|
|
|
|
self._add_edx4edx()
|
|
course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
|
|
self.assertIsNotNone(course)
|
|
|
|
self._rm_edx4edx()
|
|
course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
|
|
self.assertIsNone(course)
|
|
|
|
def test_course_info(self):
|
|
"""
|
|
Check to make sure we are getting git info for courses
|
|
"""
|
|
# Regex of first 3 columns of course information table row for
|
|
# test course loaded from git. Would not have sha1 if
|
|
# git_info_for_course failed.
|
|
table_re = re.compile(r"""
|
|
<tr>\s+
|
|
<td>edX\sAuthor\sCourse</td>\s+ # expected test git course name
|
|
<td>MITx/edx4edx/edx4edx</td>\s+ # expected test git course_id
|
|
<td>[a-fA-F\d]{40}</td> # git sha1 hash
|
|
""", re.VERBOSE)
|
|
|
|
self._setstaff_login()
|
|
self._mkdir(settings.GIT_REPO_DIR)
|
|
|
|
# Make sure we don't have any git hashes on the page
|
|
response = self.client.get(reverse('sysadmin_courses'))
|
|
self.assertNotRegexpMatches(response.content, table_re)
|
|
|
|
# Now add the course and make sure it does match
|
|
response = self._add_edx4edx()
|
|
self.assertRegexpMatches(response.content, table_re)
|
|
|
|
def test_gitlogs(self):
|
|
"""
|
|
Create a log entry and make sure it exists
|
|
"""
|
|
|
|
self._setstaff_login()
|
|
self._mkdir(settings.GIT_REPO_DIR)
|
|
|
|
self._add_edx4edx()
|
|
response = self.client.get(reverse('gitlogs'))
|
|
|
|
# Check that our earlier import has a log with a link to details
|
|
self.assertIn('/gitlogs/MITx/edx4edx/edx4edx', response.content)
|
|
|
|
response = self.client.get(
|
|
reverse('gitlogs_detail', kwargs={
|
|
'course_id': 'MITx/edx4edx/edx4edx'}))
|
|
|
|
self.assertIn('======> IMPORTING course',
|
|
response.content)
|
|
|
|
self._rm_edx4edx()
|
|
|
|
def test_gitlog_date(self):
|
|
"""
|
|
Make sure the date is timezone-aware and being converted/formatted
|
|
properly.
|
|
"""
|
|
|
|
tz_names = [
|
|
'America/New_York', # UTC - 5
|
|
'Asia/Pyongyang', # UTC + 9
|
|
'Europe/London', # UTC
|
|
'Canada/Yukon', # UTC - 8
|
|
'Europe/Moscow', # UTC + 4
|
|
]
|
|
tz_format = DEFAULT_DATE_TIME_FORMAT
|
|
|
|
self._setstaff_login()
|
|
self._mkdir(settings.GIT_REPO_DIR)
|
|
|
|
self._add_edx4edx()
|
|
date = CourseImportLog.objects.first().created.replace(tzinfo=UTC)
|
|
|
|
for timezone in tz_names:
|
|
with (override_settings(TIME_ZONE=timezone)):
|
|
date_text = get_time_display(date, tz_format, settings.TIME_ZONE)
|
|
response = self.client.get(reverse('gitlogs'))
|
|
self.assertIn(date_text, response.content.decode('UTF-8'))
|
|
|
|
self._rm_edx4edx()
|
|
|
|
def test_gitlog_bad_course(self):
|
|
"""
|
|
Make sure we gracefully handle courses that don't exist.
|
|
"""
|
|
self._setstaff_login()
|
|
response = self.client.get(
|
|
reverse('gitlogs_detail', kwargs={
|
|
'course_id': 'Not/Real/Testing'}))
|
|
self.assertEqual(404, response.status_code)
|
|
|
|
def test_gitlog_no_logs(self):
|
|
"""
|
|
Make sure the template behaves well when rendered despite there not being any logs.
|
|
(This is for courses imported using methods other than the git_add_course command)
|
|
"""
|
|
|
|
self._setstaff_login()
|
|
self._mkdir(settings.GIT_REPO_DIR)
|
|
|
|
self._add_edx4edx()
|
|
|
|
# Simulate a lack of git import logs
|
|
import_logs = CourseImportLog.objects.all()
|
|
import_logs.delete()
|
|
|
|
response = self.client.get(
|
|
reverse('gitlogs_detail', kwargs={
|
|
'course_id': 'MITx/edx4edx/edx4edx'
|
|
})
|
|
)
|
|
self.assertIn(
|
|
'No git import logs have been recorded for this course.',
|
|
response.content
|
|
)
|
|
|
|
self._rm_edx4edx()
|
|
|
|
def test_gitlog_pagination_out_of_range_invalid(self):
|
|
"""
|
|
Make sure the pagination behaves properly when the requested page is out
|
|
of range.
|
|
"""
|
|
|
|
self._setstaff_login()
|
|
|
|
mongoengine.connect(TEST_MONGODB_LOG['db'])
|
|
|
|
for _ in xrange(15):
|
|
CourseImportLog(
|
|
course_id=SlashSeparatedCourseKey("test", "test", "test"),
|
|
location="location",
|
|
import_log="import_log",
|
|
git_log="git_log",
|
|
repo_dir="repo_dir",
|
|
created=datetime.now()
|
|
).save()
|
|
|
|
for page, expected in [(-1, 1), (1, 1), (2, 2), (30, 2), ('abc', 1)]:
|
|
response = self.client.get(
|
|
'{}?page={}'.format(
|
|
reverse('gitlogs'),
|
|
page
|
|
)
|
|
)
|
|
self.assertIn(
|
|
'Page {} of 2'.format(expected),
|
|
response.content
|
|
)
|
|
|
|
CourseImportLog.objects.delete()
|
|
|
|
def test_gitlog_courseteam_access(self):
|
|
"""
|
|
Ensure course team users are allowed to access only their own course.
|
|
"""
|
|
|
|
self._mkdir(settings.GIT_REPO_DIR)
|
|
|
|
self._setstaff_login()
|
|
self._add_edx4edx()
|
|
self.user.is_staff = False
|
|
self.user.save()
|
|
logged_in = self.client.login(username=self.user.username,
|
|
password='foo')
|
|
response = self.client.get(reverse('gitlogs'))
|
|
# Make sure our non privileged user doesn't have access to all logs
|
|
self.assertEqual(response.status_code, 404)
|
|
# Or specific logs
|
|
response = self.client.get(reverse('gitlogs_detail', kwargs={
|
|
'course_id': 'MITx/edx4edx/edx4edx'
|
|
}))
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
# Add user as staff in course team
|
|
def_ms = modulestore()
|
|
course = def_ms.get_course(SlashSeparatedCourseKey('MITx', 'edx4edx', 'edx4edx'))
|
|
CourseStaffRole(course.id).add_users(self.user)
|
|
|
|
self.assertTrue(CourseStaffRole(course.id).has_user(self.user))
|
|
logged_in = self.client.login(username=self.user.username,
|
|
password='foo')
|
|
self.assertTrue(logged_in)
|
|
|
|
response = self.client.get(
|
|
reverse('gitlogs_detail', kwargs={
|
|
'course_id': 'MITx/edx4edx/edx4edx'
|
|
}))
|
|
self.assertIn('======> IMPORTING course',
|
|
response.content)
|
|
|
|
self._rm_edx4edx()
|