""" Tests for the EdxNotes app. """ import json import jwt from mock import patch, MagicMock from unittest import skipUnless from datetime import datetime from edxmako.shortcuts import render_to_string from edxnotes import helpers from edxnotes.decorators import edxnotes from edxnotes.exceptions import EdxNotesParseError, EdxNotesServiceUnavailable from django.conf import settings from django.core.urlresolvers import reverse from django.core.exceptions import ImproperlyConfigured from oauth2_provider.tests.factories import ClientFactory from provider.oauth2.models import Client from xmodule.tabs import EdxNotesTab from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from courseware.model_data import FieldDataCache from courseware.module_render import get_module_for_descriptor from student.tests.factories import UserFactory def enable_edxnotes_for_the_course(course, user_id): """ Enable EdxNotes for the course. """ course.tabs.append(EdxNotesTab()) modulestore().update_item(course, user_id) @edxnotes class TestProblem(object): """ Test class (fake problem) decorated by edxnotes decorator. The purpose of this class is to imitate any problem. """ def __init__(self, course): self.system = MagicMock(is_author_mode=False) self.scope_ids = MagicMock(usage_id="test_usage_id") self.user = UserFactory.create(username="Joe", email="joe@example.com", password="edx") self.runtime = MagicMock(course_id=course.id, get_real_user=lambda anon_id: self.user) self.descriptor = MagicMock() self.descriptor.runtime.modulestore.get_course.return_value = course def get_html(self): """ Imitate get_html in module. """ return "original_get_html" @skipUnless(settings.FEATURES["ENABLE_EDXNOTES"], "EdxNotes feature needs to be enabled.") class EdxNotesDecoratorTest(ModuleStoreTestCase): """ Tests for edxnotes decorator. """ def setUp(self): super(EdxNotesDecoratorTest, self).setUp() ClientFactory(name="edx-notes") # Using old mongo because of locator comparison issues (see longer # note below in EdxNotesHelpersTest setUp. self.course = CourseFactory.create(edxnotes=True, default_store=ModuleStoreEnum.Type.mongo) self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx") self.client.login(username=self.user.username, password="edx") self.problem = TestProblem(self.course) @patch.dict("django.conf.settings.FEATURES", {'ENABLE_EDXNOTES': True}) @patch("edxnotes.decorators.get_endpoint") @patch("edxnotes.decorators.get_token_url") @patch("edxnotes.decorators.get_id_token") @patch("edxnotes.decorators.generate_uid") def test_edxnotes_enabled(self, mock_generate_uid, mock_get_id_token, mock_get_token_url, mock_get_endpoint): """ Tests if get_html is wrapped when feature flag is on and edxnotes are enabled for the course. """ mock_generate_uid.return_value = "uid" mock_get_id_token.return_value = "token" mock_get_token_url.return_value = "/tokenUrl" mock_get_endpoint.return_value = "/endpoint" enable_edxnotes_for_the_course(self.course, self.user.id) expected_context = { "content": "original_get_html", "uid": "uid", "edxnotes_visibility": "true", "params": { "usageId": u"test_usage_id", "courseId": unicode(self.course.id).encode("utf-8"), "token": "token", "tokenUrl": "/tokenUrl", "endpoint": "/endpoint", "debug": settings.DEBUG, "eventStringLimit": settings.TRACK_MAX_EVENT / 6, }, } self.assertEqual( self.problem.get_html(), render_to_string("edxnotes_wrapper.html", expected_context), ) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True}) def test_edxnotes_disabled_if_edxnotes_flag_is_false(self): """ Tests that get_html is wrapped when feature flag is on, but edxnotes are disabled for the course. """ self.assertEqual("original_get_html", self.problem.get_html()) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": False}) def test_edxnotes_disabled(self): """ Tests that get_html is not wrapped when feature flag is off. """ self.assertEqual("original_get_html", self.problem.get_html()) def test_edxnotes_studio(self): """ Tests that get_html is not wrapped when problem is rendered in Studio. """ self.problem.system.is_author_mode = True self.assertEqual("original_get_html", self.problem.get_html()) def test_edxnotes_harvard_notes_enabled(self): """ Tests that get_html is not wrapped when Harvard Annotation Tool is enabled. """ self.course.advanced_modules = ["videoannotation", "imageannotation", "textannotation"] enable_edxnotes_for_the_course(self.course, self.user.id) self.assertEqual("original_get_html", self.problem.get_html()) @skipUnless(settings.FEATURES["ENABLE_EDXNOTES"], "EdxNotes feature needs to be enabled.") class EdxNotesHelpersTest(ModuleStoreTestCase): """ Tests for EdxNotes helpers. """ def setUp(self): """ Setup a dummy course content. """ super(EdxNotesHelpersTest, self).setUp() # There are many tests that are comparing locators as returned from helper methods. When using # the split modulestore, some of those locators have version and branch information, but the # comparison values do not. This needs further investigation in order to enable these tests # with the split modulestore. with self.store.default_store(ModuleStoreEnum.Type.mongo): ClientFactory(name="edx-notes") self.course = CourseFactory.create() self.chapter = ItemFactory.create(category="chapter", parent_location=self.course.location) self.chapter_2 = ItemFactory.create(category="chapter", parent_location=self.course.location) self.sequential = ItemFactory.create(category="sequential", parent_location=self.chapter.location) self.vertical = ItemFactory.create(category="vertical", parent_location=self.sequential.location) self.html_module_1 = ItemFactory.create(category="html", parent_location=self.vertical.location) self.html_module_2 = ItemFactory.create(category="html", parent_location=self.vertical.location) self.vertical_with_container = ItemFactory.create( category='vertical', parent_location=self.sequential.location ) self.child_container = ItemFactory.create( category='split_test', parent_location=self.vertical_with_container.location) self.child_vertical = ItemFactory.create(category='vertical', parent_location=self.child_container.location) self.child_html_module = ItemFactory.create(category="html", parent_location=self.child_vertical.location) # Read again so that children lists are accurate self.course = self.store.get_item(self.course.location) self.chapter = self.store.get_item(self.chapter.location) self.chapter_2 = self.store.get_item(self.chapter_2.location) self.sequential = self.store.get_item(self.sequential.location) self.vertical = self.store.get_item(self.vertical.location) self.vertical_with_container = self.store.get_item(self.vertical_with_container.location) self.child_container = self.store.get_item(self.child_container.location) self.child_vertical = self.store.get_item(self.child_vertical.location) self.child_html_module = self.store.get_item(self.child_html_module.location) self.user = UserFactory.create(username="Joe", email="joe@example.com", password="edx") self.client.login(username=self.user.username, password="edx") def _get_unit_url(self, course, chapter, section, position=1): """ Returns `jump_to_id` url for the `vertical`. """ return reverse('courseware_position', kwargs={ 'course_id': course.id, 'chapter': chapter.url_name, 'section': section.url_name, 'position': position, }) def test_edxnotes_not_enabled(self): """ Tests that edxnotes are disabled when the course tab configuration does NOT contain a tab with type "edxnotes." """ self.course.tabs = [] self.assertFalse(helpers.is_feature_enabled(self.course)) def test_edxnotes_harvard_notes_enabled(self): """ Tests that edxnotes are disabled when Harvard Annotation Tool is enabled. """ self.course.advanced_modules = ["foo", "imageannotation", "boo"] self.assertFalse(helpers.is_feature_enabled(self.course)) self.course.advanced_modules = ["foo", "boo", "videoannotation"] self.assertFalse(helpers.is_feature_enabled(self.course)) self.course.advanced_modules = ["textannotation", "foo", "boo"] self.assertFalse(helpers.is_feature_enabled(self.course)) self.course.advanced_modules = ["textannotation", "videoannotation", "imageannotation"] self.assertFalse(helpers.is_feature_enabled(self.course)) def test_edxnotes_enabled(self): """ Tests that edxnotes are enabled when the course tab configuration contains a tab with type "edxnotes." """ self.course.tabs = [{"type": "foo"}, {"name": "Notes", "type": "edxnotes"}, {"type": "bar"}] self.assertTrue(helpers.is_feature_enabled(self.course)) def test_get_endpoint(self): """ Tests that storage_url method returns appropriate values. """ # url ends with "/" with patch.dict("django.conf.settings.EDXNOTES_INTERFACE", {"url": "http://example.com/"}): self.assertEqual("http://example.com/", helpers.get_endpoint()) # url doesn't have "/" at the end with patch.dict("django.conf.settings.EDXNOTES_INTERFACE", {"url": "http://example.com"}): self.assertEqual("http://example.com/", helpers.get_endpoint()) # url with path that starts with "/" with patch.dict("django.conf.settings.EDXNOTES_INTERFACE", {"url": "http://example.com"}): self.assertEqual("http://example.com/some_path/", helpers.get_endpoint("/some_path")) # url with path without "/" with patch.dict("django.conf.settings.EDXNOTES_INTERFACE", {"url": "http://example.com"}): self.assertEqual("http://example.com/some_path/", helpers.get_endpoint("some_path/")) # url is not configured with patch.dict("django.conf.settings.EDXNOTES_INTERFACE", {"url": None}): self.assertRaises(ImproperlyConfigured, helpers.get_endpoint) @patch("edxnotes.helpers.requests.get") def test_get_notes_correct_data(self, mock_get): """ Tests the result if correct data is received. """ mock_get.return_value.content = json.dumps([ { u"quote": u"quote text", u"text": u"text", u"usage_id": unicode(self.html_module_1.location), u"updated": datetime(2014, 11, 19, 8, 5, 16, 00000).isoformat(), }, { u"quote": u"quote text", u"text": u"text", u"usage_id": unicode(self.html_module_2.location), u"updated": datetime(2014, 11, 19, 8, 6, 16, 00000).isoformat(), } ]) self.assertItemsEqual( [ { u"quote": u"quote text", u"text": u"text", u"chapter": { u"display_name": self.chapter.display_name_with_default, u"index": 0, u"location": unicode(self.chapter.location), u"children": [unicode(self.sequential.location)] }, u"section": { u"display_name": self.sequential.display_name_with_default, u"location": unicode(self.sequential.location), u"children": [unicode(self.vertical.location), unicode(self.vertical_with_container.location)] }, u"unit": { u"url": self._get_unit_url(self.course, self.chapter, self.sequential), u"display_name": self.vertical.display_name_with_default, u"location": unicode(self.vertical.location), }, u"usage_id": unicode(self.html_module_2.location), u"updated": "Nov 19, 2014 at 08:06 UTC", }, { u"quote": u"quote text", u"text": u"text", u"chapter": { u"display_name": self.chapter.display_name_with_default, u"index": 0, u"location": unicode(self.chapter.location), u"children": [unicode(self.sequential.location)] }, u"section": { u"display_name": self.sequential.display_name_with_default, u"location": unicode(self.sequential.location), u"children": [ unicode(self.vertical.location), unicode(self.vertical_with_container.location)] }, u"unit": { u"url": self._get_unit_url(self.course, self.chapter, self.sequential), u"display_name": self.vertical.display_name_with_default, u"location": unicode(self.vertical.location), }, u"usage_id": unicode(self.html_module_1.location), u"updated": "Nov 19, 2014 at 08:05 UTC", }, ], json.loads(helpers.get_notes(self.user, self.course)) ) @patch("edxnotes.helpers.requests.get") def test_get_notes_json_error(self, mock_get): """ Tests the result if incorrect json is received. """ mock_get.return_value.content = "Error" self.assertIsNone(helpers.get_notes(self.user, self.course)) @patch("edxnotes.helpers.requests.get") def test_get_notes_empty_collection(self, mock_get): """ Tests the result if an empty collection is received. """ mock_get.return_value.content = json.dumps([]) self.assertIsNone(helpers.get_notes(self.user, self.course)) @patch("edxnotes.helpers.requests.get") def test_search_correct_data(self, mock_get): """ Tests the result if correct data is received. """ mock_get.return_value.content = json.dumps({ "total": 2, "rows": [ { u"quote": u"quote text", u"text": u"text", u"usage_id": unicode(self.html_module_1.location), u"updated": datetime(2014, 11, 19, 8, 5, 16, 00000).isoformat(), }, { u"quote": u"quote text", u"text": u"text", u"usage_id": unicode(self.html_module_2.location), u"updated": datetime(2014, 11, 19, 8, 6, 16, 00000).isoformat(), } ] }) self.assertItemsEqual( { "total": 2, "rows": [ { u"quote": u"quote text", u"text": u"text", u"chapter": { u"display_name": self.chapter.display_name_with_default, u"index": 0, u"location": unicode(self.chapter.location), u"children": [unicode(self.sequential.location)] }, u"section": { u"display_name": self.sequential.display_name_with_default, u"location": unicode(self.sequential.location), u"children": [ unicode(self.vertical.location), unicode(self.vertical_with_container.location)] }, u"unit": { u"url": self._get_unit_url(self.course, self.chapter, self.sequential), u"display_name": self.vertical.display_name_with_default, u"location": unicode(self.vertical.location), }, u"usage_id": unicode(self.html_module_2.location), u"updated": "Nov 19, 2014 at 08:06 UTC", }, { u"quote": u"quote text", u"text": u"text", u"chapter": { u"display_name": self.chapter.display_name_with_default, u"index": 0, u"location": unicode(self.chapter.location), u"children": [unicode(self.sequential.location)] }, u"section": { u"display_name": self.sequential.display_name_with_default, u"location": unicode(self.sequential.location), u"children": [ unicode(self.vertical.location), unicode(self.vertical_with_container.location)] }, u"unit": { u"url": self._get_unit_url(self.course, self.chapter, self.sequential), u"display_name": self.vertical.display_name_with_default, u"location": unicode(self.vertical.location), }, u"usage_id": unicode(self.html_module_1.location), u"updated": "Nov 19, 2014 at 08:05 UTC", }, ] }, json.loads(helpers.search(self.user, self.course, "test")) ) @patch("edxnotes.helpers.requests.get") def test_search_json_error(self, mock_get): """ Tests the result if incorrect json is received. """ mock_get.return_value.content = "Error" self.assertRaises(EdxNotesParseError, helpers.search, self.user, self.course, "test") @patch("edxnotes.helpers.requests.get") def test_search_wrong_data_format(self, mock_get): """ Tests the result if incorrect data structure is received. """ mock_get.return_value.content = json.dumps({"1": 2}) self.assertRaises(EdxNotesParseError, helpers.search, self.user, self.course, "test") @patch("edxnotes.helpers.requests.get") def test_search_empty_collection(self, mock_get): """ Tests no results. """ mock_get.return_value.content = json.dumps({ "total": 0, "rows": [] }) self.assertItemsEqual( { "total": 0, "rows": [] }, json.loads(helpers.search(self.user, self.course, "test")) ) def test_preprocess_collection_escaping(self): """ Tests the result if appropriate module is not found. """ initial_collection = [{ u"quote": u"test <script>alert('test')</script>", u"text": u"text \"<>&'", u"usage_id": unicode(self.html_module_1.location), u"updated": datetime(2014, 11, 19, 8, 5, 16, 00000).isoformat() }] self.assertItemsEqual( [{ u"quote": u"test <script>alert('test')</script>", u"text": u'text "<>&\'', u"chapter": { u"display_name": self.chapter.display_name_with_default, u"index": 0, u"location": unicode(self.chapter.location), u"children": [unicode(self.sequential.location)] }, u"section": { u"display_name": self.sequential.display_name_with_default, u"location": unicode(self.sequential.location), u"children": [unicode(self.vertical.location), unicode(self.vertical_with_container.location)] }, u"unit": { u"url": self._get_unit_url(self.course, self.chapter, self.sequential), u"display_name": self.vertical.display_name_with_default, u"location": unicode(self.vertical.location), }, u"usage_id": unicode(self.html_module_1.location), u"updated": datetime(2014, 11, 19, 8, 5, 16, 00000), }], helpers.preprocess_collection(self.user, self.course, initial_collection) ) def test_preprocess_collection_no_item(self): """ Tests the result if appropriate module is not found. """ initial_collection = [ { u"quote": u"quote text", u"text": u"text", u"usage_id": unicode(self.html_module_1.location), u"updated": datetime(2014, 11, 19, 8, 5, 16, 00000).isoformat() }, { u"quote": u"quote text", u"text": u"text", u"usage_id": unicode(self.course.id.make_usage_key("html", "test_item")), u"updated": datetime(2014, 11, 19, 8, 6, 16, 00000).isoformat() }, ] self.assertItemsEqual( [{ u"quote": u"quote text", u"text": u"text", u"chapter": { u"display_name": self.chapter.display_name_with_default, u"index": 0, u"location": unicode(self.chapter.location), u"children": [unicode(self.sequential.location)] }, u"section": { u"display_name": self.sequential.display_name_with_default, u"location": unicode(self.sequential.location), u"children": [unicode(self.vertical.location), unicode(self.vertical_with_container.location)] }, u"unit": { u"url": self._get_unit_url(self.course, self.chapter, self.sequential), u"display_name": self.vertical.display_name_with_default, u"location": unicode(self.vertical.location), }, u"usage_id": unicode(self.html_module_1.location), u"updated": datetime(2014, 11, 19, 8, 5, 16, 00000), }], helpers.preprocess_collection(self.user, self.course, initial_collection) ) def test_preprocess_collection_has_access(self): """ Tests the result if the user does not have access to some of the modules. """ initial_collection = [ { u"quote": u"quote text", u"text": u"text", u"usage_id": unicode(self.html_module_1.location), u"updated": datetime(2014, 11, 19, 8, 5, 16, 00000).isoformat(), }, { u"quote": u"quote text", u"text": u"text", u"usage_id": unicode(self.html_module_2.location), u"updated": datetime(2014, 11, 19, 8, 6, 16, 00000).isoformat(), }, ] self.html_module_2.visible_to_staff_only = True self.store.update_item(self.html_module_2, self.user.id) self.assertItemsEqual( [{ u"quote": u"quote text", u"text": u"text", u"chapter": { u"display_name": self.chapter.display_name_with_default, u"index": 0, u"location": unicode(self.chapter.location), u"children": [unicode(self.sequential.location)] }, u"section": { u"display_name": self.sequential.display_name_with_default, u"location": unicode(self.sequential.location), u"children": [unicode(self.vertical.location), unicode(self.vertical_with_container.location)] }, u"unit": { u"url": self._get_unit_url(self.course, self.chapter, self.sequential), u"display_name": self.vertical.display_name_with_default, u"location": unicode(self.vertical.location), }, u"usage_id": unicode(self.html_module_1.location), u"updated": datetime(2014, 11, 19, 8, 5, 16, 00000), }], helpers.preprocess_collection(self.user, self.course, initial_collection) ) @patch("edxnotes.helpers.has_access") @patch("edxnotes.helpers.modulestore") def test_preprocess_collection_no_unit(self, mock_modulestore, mock_has_access): """ Tests the result if the unit does not exist. """ store = MagicMock() store.get_item().get_parent.return_value = None mock_modulestore.return_value = store mock_has_access.return_value = True initial_collection = [{ u"quote": u"quote text", u"text": u"text", u"usage_id": unicode(self.html_module_1.location), u"updated": datetime(2014, 11, 19, 8, 5, 16, 00000).isoformat(), }] self.assertItemsEqual( [], helpers.preprocess_collection(self.user, self.course, initial_collection) ) def test_get_parent_unit(self): """ Tests `get_parent_unit` method for the successful result. """ parent = helpers.get_parent_unit(self.html_module_1) self.assertEqual(parent.location, self.vertical.location) parent = helpers.get_parent_unit(self.child_html_module) self.assertEqual(parent.location, self.vertical_with_container.location) self.assertIsNone(helpers.get_parent_unit(None)) self.assertIsNone(helpers.get_parent_unit(self.course)) self.assertIsNone(helpers.get_parent_unit(self.chapter)) self.assertIsNone(helpers.get_parent_unit(self.sequential)) def test_get_module_context_sequential(self): """ Tests `get_module_context` method for the sequential. """ self.assertDictEqual( { u"display_name": self.sequential.display_name_with_default, u"location": unicode(self.sequential.location), u"children": [unicode(self.vertical.location), unicode(self.vertical_with_container.location)], }, helpers.get_module_context(self.course, self.sequential) ) def test_get_module_context_html_component(self): """ Tests `get_module_context` method for the components. """ self.assertDictEqual( { u"display_name": self.html_module_1.display_name_with_default, u"location": unicode(self.html_module_1.location), }, helpers.get_module_context(self.course, self.html_module_1) ) def test_get_module_context_chapter(self): """ Tests `get_module_context` method for the chapters. """ self.assertDictEqual( { u"display_name": self.chapter.display_name_with_default, u"index": 0, u"location": unicode(self.chapter.location), u"children": [unicode(self.sequential.location)], }, helpers.get_module_context(self.course, self.chapter) ) self.assertDictEqual( { u"display_name": self.chapter_2.display_name_with_default, u"index": 1, u"location": unicode(self.chapter_2.location), u"children": [], }, helpers.get_module_context(self.course, self.chapter_2) ) @patch.dict("django.conf.settings.EDXNOTES_INTERFACE", {"url": "http://example.com"}) @patch("edxnotes.helpers.anonymous_id_for_user") @patch("edxnotes.helpers.get_id_token") @patch("edxnotes.helpers.requests.get") def test_send_request_with_query_string(self, mock_get, mock_get_id_token, mock_anonymous_id_for_user): """ Tests that requests are send with correct information. """ mock_get_id_token.return_value = "test_token" mock_anonymous_id_for_user.return_value = "anonymous_id" helpers.send_request( self.user, self.course.id, path="test", query_string="text" ) mock_get.assert_called_with( "http://example.com/test/", headers={ "x-annotator-auth-token": "test_token" }, params={ "user": "anonymous_id", "course_id": unicode(self.course.id), "text": "text", "highlight": True, "highlight_tag": "span", "highlight_class": "note-highlight", } ) @patch.dict("django.conf.settings.EDXNOTES_INTERFACE", {"url": "http://example.com"}) @patch("edxnotes.helpers.anonymous_id_for_user") @patch("edxnotes.helpers.get_id_token") @patch("edxnotes.helpers.requests.get") def test_send_request_without_query_string(self, mock_get, mock_get_id_token, mock_anonymous_id_for_user): """ Tests that requests are send with correct information. """ mock_get_id_token.return_value = "test_token" mock_anonymous_id_for_user.return_value = "anonymous_id" helpers.send_request( self.user, self.course.id, path="test" ) mock_get.assert_called_with( "http://example.com/test/", headers={ "x-annotator-auth-token": "test_token" }, params={ "user": "anonymous_id", "course_id": unicode(self.course.id), } ) def test_get_course_position_no_chapter(self): """ Returns `None` if no chapter found. """ mock_course_module = MagicMock() mock_course_module.position = 3 mock_course_module.get_display_items.return_value = [] self.assertIsNone(helpers.get_course_position(mock_course_module)) def test_get_course_position_to_chapter(self): """ Returns a position that leads to COURSE/CHAPTER if this isn't the users's first time. """ mock_course_module = MagicMock(id=self.course.id, position=3) mock_chapter = MagicMock() mock_chapter.url_name = 'chapter_url_name' mock_chapter.display_name_with_default = 'Test Chapter Display Name' mock_course_module.get_display_items.return_value = [mock_chapter] self.assertEqual(helpers.get_course_position(mock_course_module), { 'display_name': 'Test Chapter Display Name', 'url': '/courses/{}/courseware/chapter_url_name/'.format(self.course.id), }) def test_get_course_position_no_section(self): """ Returns `None` if no section found. """ mock_course_module = MagicMock(id=self.course.id, position=None) mock_course_module.get_display_items.return_value = [MagicMock()] self.assertIsNone(helpers.get_course_position(mock_course_module)) def test_get_course_position_to_section(self): """ Returns a position that leads to COURSE/CHAPTER/SECTION if this is the user's first time. """ mock_course_module = MagicMock(id=self.course.id, position=None) mock_chapter = MagicMock() mock_chapter.url_name = 'chapter_url_name' mock_course_module.get_display_items.return_value = [mock_chapter] mock_section = MagicMock() mock_section.url_name = 'section_url_name' mock_section.display_name_with_default = 'Test Section Display Name' mock_chapter.get_display_items.return_value = [mock_section] mock_section.get_display_items.return_value = [MagicMock()] self.assertEqual(helpers.get_course_position(mock_course_module), { 'display_name': 'Test Section Display Name', 'url': '/courses/{}/courseware/chapter_url_name/section_url_name/'.format(self.course.id), }) def test_get_index(self): """ Tests `get_index` method returns unit url. """ children = self.sequential.children self.assertEqual(0, helpers.get_index(unicode(self.vertical.location), children)) self.assertEqual(1, helpers.get_index(unicode(self.vertical_with_container.location), children)) @skipUnless(settings.FEATURES["ENABLE_EDXNOTES"], "EdxNotes feature needs to be enabled.") class EdxNotesViewsTest(ModuleStoreTestCase): """ Tests for EdxNotes views. """ def setUp(self): ClientFactory(name="edx-notes") super(EdxNotesViewsTest, self).setUp() self.course = CourseFactory.create(edxnotes=True) self.user = UserFactory.create(username="Bob", email="bob@example.com", password="edx") self.client.login(username=self.user.username, password="edx") self.notes_page_url = reverse("edxnotes", args=[unicode(self.course.id)]) self.search_url = reverse("search_notes", args=[unicode(self.course.id)]) self.get_token_url = reverse("get_token", args=[unicode(self.course.id)]) self.visibility_url = reverse("edxnotes_visibility", args=[unicode(self.course.id)]) def _get_course_module(self): """ Returns the course module. """ field_data_cache = FieldDataCache([self.course], self.course.id, self.user) return get_module_for_descriptor(self.user, MagicMock(), self.course, field_data_cache, self.course.id) # pylint: disable=unused-argument @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True}) @patch("edxnotes.views.get_notes", return_value=[]) def test_edxnotes_view_is_enabled(self, mock_get_notes): """ Tests that appropriate view is received if EdxNotes feature is enabled. """ enable_edxnotes_for_the_course(self.course, self.user.id) response = self.client.get(self.notes_page_url) self.assertContains(response, 'Highlights and notes you\'ve made in course content') @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": False}) def test_edxnotes_view_is_disabled(self): """ Tests that 404 status code is received if EdxNotes feature is disabled. """ response = self.client.get(self.notes_page_url) self.assertEqual(response.status_code, 404) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True}) @patch("edxnotes.views.get_notes") def test_edxnotes_view_404_service_unavailable(self, mock_get_notes): """ Tests that 404 status code is received if EdxNotes service is unavailable. """ mock_get_notes.side_effect = EdxNotesServiceUnavailable enable_edxnotes_for_the_course(self.course, self.user.id) response = self.client.get(self.notes_page_url) self.assertEqual(response.status_code, 404) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True}) @patch("edxnotes.views.search") def test_search_notes_successfully_respond(self, mock_search): """ Tests that `search_notes` successfully respond if EdxNotes feature is enabled. """ mock_search.return_value = json.dumps({ "total": 0, "rows": [], }) enable_edxnotes_for_the_course(self.course, self.user.id) response = self.client.get(self.search_url, {"text": "test"}) self.assertEqual(json.loads(response.content), { "total": 0, "rows": [], }) self.assertEqual(response.status_code, 200) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": False}) @patch("edxnotes.views.search") def test_search_notes_is_disabled(self, mock_search): """ Tests that 404 status code is received if EdxNotes feature is disabled. """ mock_search.return_value = json.dumps({ "total": 0, "rows": [], }) response = self.client.get(self.search_url, {"text": "test"}) self.assertEqual(response.status_code, 404) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True}) @patch("edxnotes.views.search") def test_search_404_service_unavailable(self, mock_search): """ Tests that 404 status code is received if EdxNotes service is unavailable. """ mock_search.side_effect = EdxNotesServiceUnavailable enable_edxnotes_for_the_course(self.course, self.user.id) response = self.client.get(self.search_url, {"text": "test"}) self.assertEqual(response.status_code, 500) self.assertIn("error", response.content) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True}) @patch("edxnotes.views.search") def test_search_notes_without_required_parameters(self, mock_search): """ Tests that 400 status code is received if the required parameters were not sent. """ mock_search.return_value = json.dumps({ "total": 0, "rows": [], }) enable_edxnotes_for_the_course(self.course, self.user.id) response = self.client.get(self.search_url) self.assertEqual(response.status_code, 400) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True}) @patch("edxnotes.views.search") def test_search_notes_exception(self, mock_search): """ Tests that 500 status code is received if invalid data was received from EdXNotes service. """ mock_search.side_effect = EdxNotesParseError enable_edxnotes_for_the_course(self.course, self.user.id) response = self.client.get(self.search_url, {"text": "test"}) self.assertEqual(response.status_code, 500) self.assertIn("error", response.content) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True}) def test_get_id_token(self): """ Test generation of ID Token. """ response = self.client.get(self.get_token_url) self.assertEqual(response.status_code, 200) client = Client.objects.get(name='edx-notes') jwt.decode(response.content, client.client_secret, audience=client.client_id) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True}) def test_get_id_token_anonymous(self): """ Test that generation of ID Token does not work for anonymous user. """ self.client.logout() response = self.client.get(self.get_token_url) self.assertEqual(response.status_code, 302) def test_edxnotes_visibility(self): """ Can update edxnotes_visibility value successfully. """ enable_edxnotes_for_the_course(self.course, self.user.id) response = self.client.post( self.visibility_url, data=json.dumps({"visibility": False}), content_type="application/json", ) self.assertEqual(response.status_code, 200) course_module = self._get_course_module() self.assertFalse(course_module.edxnotes_visibility) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": False}) def test_edxnotes_visibility_if_feature_is_disabled(self): """ Tests that 404 response is received if EdxNotes feature is disabled. """ response = self.client.post(self.visibility_url) self.assertEqual(response.status_code, 404) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True}) def test_edxnotes_visibility_invalid_json(self): """ Tests that 400 response is received if invalid JSON is sent. """ enable_edxnotes_for_the_course(self.course, self.user.id) response = self.client.post( self.visibility_url, data="string", content_type="application/json", ) self.assertEqual(response.status_code, 400) @patch.dict("django.conf.settings.FEATURES", {"ENABLE_EDXNOTES": True}) def test_edxnotes_visibility_key_error(self): """ Tests that 400 response is received if invalid data structure is sent. """ enable_edxnotes_for_the_course(self.course, self.user.id) response = self.client.post( self.visibility_url, data=json.dumps({'test_key': 1}), content_type="application/json", ) self.assertEqual(response.status_code, 400)