Beispiel #1
0
def get_request_for_user(user):
    """Create a request object for user."""

    request = RequestFactory()
    request.user = user
    request.META = {}
    request.is_secure = lambda: True
    request.get_host = lambda: "edx.org"
    return request
Beispiel #2
0
def get_request_for_user(user):
    """Create a request object for user."""

    request = RequestFactory()
    request.user = user
    request.META = {}
    request.is_secure = lambda: True
    request.get_host = lambda: "edx.org"
    return request
Beispiel #3
0
def get_request_for_user(user):
    """Create a request object for user."""
    request = RequestFactory()
    request.user = user
    request.COOKIES = {}
    request.META = {}
    request.is_secure = lambda: True
    request.get_host = lambda: "edx.org"
    request.method = 'GET'
    request.GET = {}
    request.POST = {}
    crum.set_current_request(request)
    return request
Beispiel #4
0
class DownstreamRequestsTest(TestCase):
    def setUp(self):
        self.request = RequestFactory().get('/foo?bar=1')
        self.request.META['HTTP_my_header'] = 'foo'
        self.downstream_request = DownstreamRequest(self.request)

    def test_has_a_valid_query_string_attribute(self):
        self.assertEqual(self.downstream_request.query_string, 'bar=1')

    def test_proxy_attributes_to_their_Django_request_isntance(self):
        self.assertEqual(self.request.get_host(),
                         self.downstream_request.get_host())

    def test_x_forwarded_for_attribute_returns_requestor(self):
        self.assertEqual(self.downstream_request.x_forwarded_for, '127.0.0.1')

    def test_header_attribute_returns_header_set_containing_http_headers(self):
        self.assertEqual(self.downstream_request.headers['My-Header'], 'foo')
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="******", email="*****@*****.**", password="******")
            self.client.login(username=self.user.username, password="******")

        self.request = RequestFactory().request()
        self.request.user = self.user

    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_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))

    @ddt.unpack
    @ddt.data(
        {'_edxnotes': True},
        {'_edxnotes': False}
    )
    def test_is_feature_enabled(self, _edxnotes):
        """
        Tests that is_feature_enabled shows correct behavior.
        """
        self.course.edxnotes = _edxnotes
        self.assertEqual(helpers.is_feature_enabled(self.course), _edxnotes)

    @ddt.data(
        helpers.get_public_endpoint,
        helpers.get_internal_endpoint,
    )
    def test_get_endpoints(self, get_endpoint_function):
        """
        Test that the get_public_endpoint and get_internal_endpoint functions
        return appropriate values.
        """
        @contextmanager
        def patch_edxnotes_api_settings(url):
            """
            Convenience function for patching both EDXNOTES_PUBLIC_API and
            EDXNOTES_INTERNAL_API.
            """
            with override_settings(EDXNOTES_PUBLIC_API=url):
                with override_settings(EDXNOTES_INTERNAL_API=url):
                    yield

        # url ends with "/"
        with patch_edxnotes_api_settings("http://example.com/"):
            self.assertEqual("http://example.com/", get_endpoint_function())

        # url doesn't have "/" at the end
        with patch_edxnotes_api_settings("http://example.com"):
            self.assertEqual("http://example.com/", get_endpoint_function())

        # url with path that starts with "/"
        with patch_edxnotes_api_settings("http://example.com"):
            self.assertEqual("http://example.com/some_path/", get_endpoint_function("/some_path"))

        # url with path without "/"
        with patch_edxnotes_api_settings("http://example.com"):
            self.assertEqual("http://example.com/some_path/", get_endpoint_function("some_path/"))

        # url is not configured
        with patch_edxnotes_api_settings(None):
            self.assertRaises(ImproperlyConfigured, get_endpoint_function)

    @patch("edxnotes.helpers.requests.get", autospec=True)
    def test_get_notes_correct_data(self, mock_get):
        """
        Tests the result if correct data is received.
        """
        mock_get.return_value.content = json.dumps(
            {
                "total": 2,
                "current_page": 1,
                "start": 0,
                "next": None,
                "previous": None,
                "num_pages": 1,
                "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(
            {
                "count": 2,
                "current_page": 1,
                "start": 0,
                "next": None,
                "previous": None,
                "num_pages": 1,
                "results": [
                    {
                        u"quote": u"quote text",
                        u"text": u"text",
                        u"chapter": {
                            u"display_name": self.chapter.display_name_with_default_escaped,
                            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_escaped,
                            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_escaped,
                            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_escaped,
                            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_escaped,
                            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_escaped,
                            u"location": unicode(self.vertical.location),
                        },
                        u"usage_id": unicode(self.html_module_1.location),
                        u"updated": "Nov 19, 2014 at 08:05 UTC",
                    },
                ]
            },
            helpers.get_notes(self.request, self.course)
        )

    @patch("edxnotes.helpers.requests.get", autospec=True)
    def test_get_notes_json_error(self, mock_get):
        """
        Tests the result if incorrect json is received.
        """
        mock_get.return_value.content = "Error"
        self.assertRaises(EdxNotesParseError, helpers.get_notes, self.request, self.course)

    @patch("edxnotes.helpers.requests.get", autospec=True)
    def test_get_notes_empty_collection(self, mock_get):
        """
        Tests the result if an empty response is received.
        """
        mock_get.return_value.content = json.dumps({})
        self.assertRaises(EdxNotesParseError, helpers.get_notes, self.request, self.course)

    @patch("edxnotes.helpers.requests.get", autospec=True)
    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,
            "current_page": 1,
            "start": 0,
            "next": None,
            "previous": None,
            "num_pages": 1,
            "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(
            {
                "count": 2,
                "current_page": 1,
                "start": 0,
                "next": None,
                "previous": None,
                "num_pages": 1,
                "results": [
                    {
                        u"quote": u"quote text",
                        u"text": u"text",
                        u"chapter": {
                            u"display_name": self.chapter.display_name_with_default_escaped,
                            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_escaped,
                            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_escaped,
                            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_escaped,
                            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_escaped,
                            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_escaped,
                            u"location": unicode(self.vertical.location),
                        },
                        u"usage_id": unicode(self.html_module_1.location),
                        u"updated": "Nov 19, 2014 at 08:05 UTC",
                    },
                ]
            },
            helpers.get_notes(self.request, self.course)
        )

    @patch("edxnotes.helpers.requests.get", autospec=True)
    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.get_notes, self.request, self.course)

    @patch("edxnotes.helpers.requests.get", autospec=True)
    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.get_notes, self.request, self.course)

    @patch("edxnotes.helpers.requests.get", autospec=True)
    def test_search_empty_collection(self, mock_get):
        """
        Tests no results.
        """
        mock_get.return_value.content = json.dumps(NOTES_API_EMPTY_RESPONSE)
        self.assertItemsEqual(
            NOTES_VIEW_EMPTY_RESPONSE,
            helpers.get_notes(self.request, self.course)
        )

    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_escaped,
                    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_escaped,
                    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_escaped,
                    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_escaped,
                    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_escaped,
                    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_escaped,
                    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", autospec=True)
    @patch("edxnotes.helpers.modulestore", autospec=True)
    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)
        )

    @override_settings(NOTES_DISABLED_TABS=['course_structure', 'tags'])
    def test_preprocess_collection_with_disabled_tabs(self, ):
        """
        Tests that preprocess collection returns correct data if `course_structure` and `tags` are disabled.
        """
        initial_collection = [
            {
                u"quote": u"quote text1",
                u"text": u"text1",
                u"usage_id": unicode(self.html_module_1.location),
                u"updated": datetime(2016, 01, 26, 8, 5, 16, 00000).isoformat(),
            },
            {
                u"quote": u"quote text2",
                u"text": u"text2",
                u"usage_id": unicode(self.html_module_2.location),
                u"updated": datetime(2016, 01, 26, 9, 6, 17, 00000).isoformat(),
            },
        ]

        self.assertItemsEqual(
            [
                {

                    'section': {},
                    'chapter': {},
                    "unit": {
                        u"url": self._get_unit_url(self.course, self.chapter, self.sequential),
                        u"display_name": self.vertical.display_name_with_default_escaped,
                        u"location": unicode(self.vertical.location),
                    },
                    u'text': u'text1',
                    u'quote': u'quote text1',
                    u'usage_id': unicode(self.html_module_1.location),
                    u'updated': datetime(2016, 01, 26, 8, 5, 16)
                },
                {
                    'section': {},
                    'chapter': {},
                    "unit": {
                        u"url": self._get_unit_url(self.course, self.chapter, self.sequential),
                        u"display_name": self.vertical.display_name_with_default_escaped,
                        u"location": unicode(self.vertical.location),
                    },
                    u'text': u'text2',
                    u'quote': u'quote text2',
                    u'usage_id': unicode(self.html_module_2.location),
                    u'updated': datetime(2016, 01, 26, 9, 6, 17)
                }
            ],
            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_escaped,
                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_escaped,
                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_escaped,
                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_escaped,
                u"index": 1,
                u"location": unicode(self.chapter_2.location),
                u"children": [],
            },
            helpers.get_module_context(self.course, self.chapter_2)
        )

    @override_settings(EDXNOTES_PUBLIC_API="http://example.com")
    @override_settings(EDXNOTES_INTERNAL_API="http://example.com")
    @patch("edxnotes.helpers.anonymous_id_for_user", autospec=True)
    @patch("edxnotes.helpers.get_edxnotes_id_token", autospec=True)
    @patch("edxnotes.helpers.requests.get", autospec=True)
    def test_send_request_with_text_param(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",
            text="text",
            page=helpers.DEFAULT_PAGE,
            page_size=helpers.DEFAULT_PAGE_SIZE
        )
        mock_get.assert_called_with(
            "http://example.com/test/",
            headers={
                "x-annotator-auth-token": "test_token"
            },
            params={
                "user": "******",
                "course_id": unicode(self.course.id),
                "text": "text",
                "highlight": True,
                'page': 1,
                'page_size': 25,
            },
            timeout=(settings.EDXNOTES_CONNECT_TIMEOUT, settings.EDXNOTES_READ_TIMEOUT)
        )

    @override_settings(EDXNOTES_PUBLIC_API="http://example.com")
    @override_settings(EDXNOTES_INTERNAL_API="http://example.com")
    @patch("edxnotes.helpers.anonymous_id_for_user", autospec=True)
    @patch("edxnotes.helpers.get_edxnotes_id_token", autospec=True)
    @patch("edxnotes.helpers.requests.get", autospec=True)
    def test_send_request_without_text_param(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", page=1, page_size=25
        )
        mock_get.assert_called_with(
            "http://example.com/test/",
            headers={
                "x-annotator-auth-token": "test_token"
            },
            params={
                "user": "******",
                "course_id": unicode(self.course.id),
                'page': helpers.DEFAULT_PAGE,
                'page_size': helpers.DEFAULT_PAGE_SIZE,
            },
            timeout=(settings.EDXNOTES_CONNECT_TIMEOUT, settings.EDXNOTES_READ_TIMEOUT)
        )

    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_escaped = '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_escaped = '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))

    @ddt.unpack
    @ddt.data(
        {'previous_api_url': None, 'next_api_url': None},
        {'previous_api_url': None, 'next_api_url': 'edxnotes/?course_id=abc&page=2&page_size=10&user=123'},
        {'previous_api_url': 'edxnotes.org/?course_id=abc&page=2&page_size=10&user=123', 'next_api_url': None},
        {
            'previous_api_url': 'edxnotes.org/?course_id=abc&page_size=10&user=123',
            'next_api_url': 'edxnotes.org/?course_id=abc&page=3&page_size=10&user=123'
        },
        {
            'previous_api_url': 'edxnotes.org/?course_id=abc&page=2&page_size=10&text=wow&user=123',
            'next_api_url': 'edxnotes.org/?course_id=abc&page=4&page_size=10&text=wow&user=123'
        },
    )
    def test_construct_url(self, previous_api_url, next_api_url):
        """
        Verify that `construct_url` works correctly.
        """
        # make absolute url
        # pylint: disable=no-member
        if self.request.is_secure():
            host = 'https://' + self.request.get_host()
        else:
            host = 'http://' + self.request.get_host()
        notes_url = host + reverse("notes", args=[unicode(self.course.id)])

        def verify_url(constructed, expected):
            """
            Verify that constructed url is correct.
            """
            # if api url is None then constructed url should also be None
            if expected is None:
                self.assertEqual(expected, constructed)
            else:
                # constructed url should startswith notes view url instead of api view url
                self.assertTrue(constructed.startswith(notes_url))

                # constructed url should not contain extra params
                self.assertNotIn('user', constructed)

                # constructed url should only has these params if present in api url
                allowed_params = ('page', 'page_size', 'text')

                # extract query params from constructed url
                parsed = urlparse.urlparse(constructed)
                params = urlparse.parse_qs(parsed.query)

                # verify that constructed url has only correct params and params have correct values
                for param, value in params.items():
                    self.assertIn(param, allowed_params)
                    self.assertIn('{}={}'.format(param, value[0]), expected)

        next_url, previous_url = helpers.construct_pagination_urls(
            self.request,
            self.course.id,
            next_api_url, previous_api_url
        )
        verify_url(next_url, next_api_url)
        verify_url(previous_url, previous_api_url)
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()
            self.client.login(username=self.user.username, password=UserFactory._DEFAULT_PASSWORD)

        self.request = RequestFactory().request()
        self.request.user = self.user

    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_harvard_notes_enabled(self):
        """
        Tests that edxnotes are disabled when Harvard Annotation Tool is enabled.
        """
        self.course.advanced_modules = ['imageannotation', 'textannotation', 'videoannotation']
        assert not helpers.is_feature_enabled(self.course, self.user)

    @ddt.data(True, False)
    def test_is_feature_enabled(self, enabled):
        """
        Tests that is_feature_enabled shows correct behavior.
        """
        course = CourseFactory(edxnotes=enabled)
        enrollment = CourseEnrollmentFactory(course_id=course.id)
        assert helpers.is_feature_enabled(course, enrollment.user) == enabled

    @ddt.data(
        helpers.get_public_endpoint,
        helpers.get_internal_endpoint,
    )
    def test_get_endpoints(self, get_endpoint_function):
        """
        Test that the get_public_endpoint and get_internal_endpoint functions
        return appropriate values.
        """
        @contextmanager
        def patch_edxnotes_api_settings(url):
            """
            Convenience function for patching both EDXNOTES_PUBLIC_API and
            EDXNOTES_INTERNAL_API.
            """
            with override_settings(EDXNOTES_PUBLIC_API=url):
                with override_settings(EDXNOTES_INTERNAL_API=url):
                    yield

        # url ends with "/"
        with patch_edxnotes_api_settings("http://example.com/"):
            self.assertEqual("http://example.com/", get_endpoint_function())

        # url doesn't have "/" at the end
        with patch_edxnotes_api_settings("http://example.com"):
            self.assertEqual("http://example.com/", get_endpoint_function())

        # url with path that starts with "/"
        with patch_edxnotes_api_settings("http://example.com"):
            self.assertEqual("http://example.com/some_path/", get_endpoint_function("/some_path"))

        # url with path without "/"
        with patch_edxnotes_api_settings("http://example.com"):
            self.assertEqual("http://example.com/some_path/", get_endpoint_function("some_path/"))

        # url is not configured
        with patch_edxnotes_api_settings(None):
            self.assertRaises(ImproperlyConfigured, get_endpoint_function)

    @patch("edxnotes.helpers.requests.get", autospec=True)
    def test_get_notes_correct_data(self, mock_get):
        """
        Tests the result if correct data is received.
        """
        mock_get.return_value.content = json.dumps(
            {
                "total": 2,
                "current_page": 1,
                "start": 0,
                "next": None,
                "previous": None,
                "num_pages": 1,
                "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(
            {
                "count": 2,
                "current_page": 1,
                "start": 0,
                "next": None,
                "previous": None,
                "num_pages": 1,
                "results": [
                    {
                        u"quote": u"quote text",
                        u"text": u"text",
                        u"chapter": {
                            u"display_name": self.chapter.display_name_with_default_escaped,
                            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_escaped,
                            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_escaped,
                            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_escaped,
                            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_escaped,
                            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_escaped,
                            u"location": unicode(self.vertical.location),
                        },
                        u"usage_id": unicode(self.html_module_1.location),
                        u"updated": "Nov 19, 2014 at 08:05 UTC",
                    },
                ]
            },
            helpers.get_notes(self.request, self.course)
        )

    @patch("edxnotes.helpers.requests.get", autospec=True)
    def test_get_notes_json_error(self, mock_get):
        """
        Tests the result if incorrect json is received.
        """
        mock_get.return_value.content = "Error"
        self.assertRaises(EdxNotesParseError, helpers.get_notes, self.request, self.course)

    @patch("edxnotes.helpers.requests.get", autospec=True)
    def test_get_notes_empty_collection(self, mock_get):
        """
        Tests the result if an empty response is received.
        """
        mock_get.return_value.content = json.dumps({})
        self.assertRaises(EdxNotesParseError, helpers.get_notes, self.request, self.course)

    @patch("edxnotes.helpers.requests.get", autospec=True)
    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,
            "current_page": 1,
            "start": 0,
            "next": None,
            "previous": None,
            "num_pages": 1,
            "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(
            {
                "count": 2,
                "current_page": 1,
                "start": 0,
                "next": None,
                "previous": None,
                "num_pages": 1,
                "results": [
                    {
                        u"quote": u"quote text",
                        u"text": u"text",
                        u"chapter": {
                            u"display_name": self.chapter.display_name_with_default_escaped,
                            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_escaped,
                            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_escaped,
                            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_escaped,
                            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_escaped,
                            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_escaped,
                            u"location": unicode(self.vertical.location),
                        },
                        u"usage_id": unicode(self.html_module_1.location),
                        u"updated": "Nov 19, 2014 at 08:05 UTC",
                    },
                ]
            },
            helpers.get_notes(self.request, self.course)
        )

    @patch("edxnotes.helpers.requests.get", autospec=True)
    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.get_notes, self.request, self.course)

    @patch("edxnotes.helpers.requests.get", autospec=True)
    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.get_notes, self.request, self.course)

    @patch("edxnotes.helpers.requests.get", autospec=True)
    def test_search_empty_collection(self, mock_get):
        """
        Tests no results.
        """
        mock_get.return_value.content = json.dumps(NOTES_API_EMPTY_RESPONSE)
        self.assertItemsEqual(
            NOTES_VIEW_EMPTY_RESPONSE,
            helpers.get_notes(self.request, self.course)
        )

    @override_settings(EDXNOTES_PUBLIC_API="http://example.com")
    @override_settings(EDXNOTES_INTERNAL_API="http://example.com")
    @patch("edxnotes.helpers.requests.delete")
    def test_delete_all_notes_for_user(self, mock_delete):
        """
        Test GDPR data deletion for Notes user_id
        """
        with mock.patch('edxnotes.helpers.get_edxnotes_id_token', return_value="test_token"):
            helpers.delete_all_notes_for_user(user=self.user, user_id="anonymous_id")
            mock_delete.assert_called_with(
                url='http://example.com/',
                headers={
                    'x-annotator-auth-token': 'test_token'
                },
                data={
                    'user_id': 'anonymous_id'
                },
                timeout=(settings.EDXNOTES_CONNECT_TIMEOUT, settings.EDXNOTES_READ_TIMEOUT)
            )

    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_escaped,
                    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_escaped,
                    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_escaped,
                    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_escaped,
                    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_escaped,
                    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_escaped,
                    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", autospec=True)
    @patch("edxnotes.helpers.modulestore", autospec=True)
    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)
        )

    @override_settings(NOTES_DISABLED_TABS=['course_structure', 'tags'])
    def test_preprocess_collection_with_disabled_tabs(self, ):
        """
        Tests that preprocess collection returns correct data if `course_structure` and `tags` are disabled.
        """
        initial_collection = [
            {
                u"quote": u"quote text1",
                u"text": u"text1",
                u"usage_id": unicode(self.html_module_1.location),
                u"updated": datetime(2016, 01, 26, 8, 5, 16, 00000).isoformat(),
            },
            {
                u"quote": u"quote text2",
                u"text": u"text2",
                u"usage_id": unicode(self.html_module_2.location),
                u"updated": datetime(2016, 01, 26, 9, 6, 17, 00000).isoformat(),
            },
        ]

        self.assertItemsEqual(
            [
                {

                    'section': {},
                    'chapter': {},
                    "unit": {
                        u"url": self._get_unit_url(self.course, self.chapter, self.sequential),
                        u"display_name": self.vertical.display_name_with_default_escaped,
                        u"location": unicode(self.vertical.location),
                    },
                    u'text': u'text1',
                    u'quote': u'quote text1',
                    u'usage_id': unicode(self.html_module_1.location),
                    u'updated': datetime(2016, 01, 26, 8, 5, 16)
                },
                {
                    'section': {},
                    'chapter': {},
                    "unit": {
                        u"url": self._get_unit_url(self.course, self.chapter, self.sequential),
                        u"display_name": self.vertical.display_name_with_default_escaped,
                        u"location": unicode(self.vertical.location),
                    },
                    u'text': u'text2',
                    u'quote': u'quote text2',
                    u'usage_id': unicode(self.html_module_2.location),
                    u'updated': datetime(2016, 01, 26, 9, 6, 17)
                }
            ],
            helpers.preprocess_collection(self.user, self.course, initial_collection)
        )

    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_escaped,
                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_escaped,
                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_escaped,
                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_escaped,
                u"index": 1,
                u"location": unicode(self.chapter_2.location),
                u"children": [],
            },
            helpers.get_module_context(self.course, self.chapter_2)
        )

    @override_settings(EDXNOTES_PUBLIC_API="http://example.com")
    @override_settings(EDXNOTES_INTERNAL_API="http://example.com")
    @patch("edxnotes.helpers.anonymous_id_for_user", autospec=True)
    @patch("edxnotes.helpers.get_edxnotes_id_token", autospec=True)
    @patch("edxnotes.helpers.requests.get", autospec=True)
    def test_send_request_with_text_param(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",
            text="text",
            page=helpers.DEFAULT_PAGE,
            page_size=helpers.DEFAULT_PAGE_SIZE
        )
        mock_get.assert_called_with(
            "http://example.com/test/",
            headers={
                "x-annotator-auth-token": "test_token"
            },
            params={
                "user": "******",
                "course_id": unicode(self.course.id),
                "text": "text",
                "highlight": True,
                'page': 1,
                'page_size': 25,
            },
            timeout=(settings.EDXNOTES_CONNECT_TIMEOUT, settings.EDXNOTES_READ_TIMEOUT)
        )

    @override_settings(EDXNOTES_PUBLIC_API="http://example.com")
    @override_settings(EDXNOTES_INTERNAL_API="http://example.com")
    @patch("edxnotes.helpers.anonymous_id_for_user", autospec=True)
    @patch("edxnotes.helpers.get_edxnotes_id_token", autospec=True)
    @patch("edxnotes.helpers.requests.get", autospec=True)
    def test_send_request_without_text_param(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", page=1, page_size=25
        )
        mock_get.assert_called_with(
            "http://example.com/test/",
            headers={
                "x-annotator-auth-token": "test_token"
            },
            params={
                "user": "******",
                "course_id": unicode(self.course.id),
                'page': helpers.DEFAULT_PAGE,
                'page_size': helpers.DEFAULT_PAGE_SIZE,
            },
            timeout=(settings.EDXNOTES_CONNECT_TIMEOUT, settings.EDXNOTES_READ_TIMEOUT)
        )

    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_escaped = '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_escaped = '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))

    @ddt.unpack
    @ddt.data(
        {'previous_api_url': None, 'next_api_url': None},
        {'previous_api_url': None, 'next_api_url': 'edxnotes/?course_id=abc&page=2&page_size=10&user=123'},
        {'previous_api_url': 'edxnotes.org/?course_id=abc&page=2&page_size=10&user=123', 'next_api_url': None},
        {
            'previous_api_url': 'edxnotes.org/?course_id=abc&page_size=10&user=123',
            'next_api_url': 'edxnotes.org/?course_id=abc&page=3&page_size=10&user=123'
        },
        {
            'previous_api_url': 'edxnotes.org/?course_id=abc&page=2&page_size=10&text=wow&user=123',
            'next_api_url': 'edxnotes.org/?course_id=abc&page=4&page_size=10&text=wow&user=123'
        },
    )
    def test_construct_url(self, previous_api_url, next_api_url):
        """
        Verify that `construct_url` works correctly.
        """
        # make absolute url
        # pylint: disable=no-member
        if self.request.is_secure():
            host = 'https://' + self.request.get_host()
        else:
            host = 'http://' + self.request.get_host()
        notes_url = host + reverse("notes", args=[unicode(self.course.id)])

        def verify_url(constructed, expected):
            """
            Verify that constructed url is correct.
            """
            # if api url is None then constructed url should also be None
            if expected is None:
                self.assertEqual(expected, constructed)
            else:
                # constructed url should startswith notes view url instead of api view url
                self.assertTrue(constructed.startswith(notes_url))

                # constructed url should not contain extra params
                self.assertNotIn('user', constructed)

                # constructed url should only has these params if present in api url
                allowed_params = ('page', 'page_size', 'text')

                # extract query params from constructed url
                parsed = urlparse.urlparse(constructed)
                params = urlparse.parse_qs(parsed.query)

                # verify that constructed url has only correct params and params have correct values
                for param, value in params.items():
                    self.assertIn(param, allowed_params)
                    self.assertIn('{}={}'.format(param, value[0]), expected)

        next_url, previous_url = helpers.construct_pagination_urls(
            self.request,
            self.course.id,
            next_api_url, previous_api_url
        )
        verify_url(next_url, next_api_url)
        verify_url(previous_url, previous_api_url)