"""Tests for simple search controller, :mod:`search.controllers.simple`."""
from unittest import TestCase, mock
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
from werkzeug import MultiDict
from werkzeug.exceptions import InternalServerError, NotFound, BadRequest
from arxiv import status
from search.domain import Query, DateRange, SimpleQuery, DocumentSet
from search.controllers import simple
from search.controllers.simple.forms import SimpleSearchForm
from search.services.index import IndexConnectionError, QueryError, \
DocumentNotFound
[docs]class TestRetrieveDocument(TestCase):
"""Tests for :func:`.simple.retrieve_document`."""
[docs] @mock.patch('search.controllers.simple.index')
def test_encounters_queryerror(self, mock_index):
"""There is a bug in the index or query."""
# We need to explicit assign the exception to the mock, otherwise the
# exception raised in the side-effect will just be a mock object (not
# inheriting from BaseException).
mock_index.QueryError = QueryError
mock_index.IndexConnectionError = IndexConnectionError
def _raiseQueryError(*args, **kwargs):
raise QueryError('What now')
mock_index.get_document.side_effect = _raiseQueryError
with self.assertRaises(InternalServerError):
try:
response_data, code, headers = simple.retrieve_document(1)
except QueryError as e:
self.fail("QueryError should be handled (caught %s)" % e)
self.assertEqual(mock_index.get_document.call_count, 1,
"A search should be attempted")
[docs] @mock.patch('search.controllers.simple.index')
def test_index_raises_connection_exception(self, mock_index):
"""Index service raises a IndexConnectionError."""
# We need to explicit assign the exception to the mock, otherwise the
# exception raised in the side-effect will just be a mock object (not
# inheriting from BaseException).
mock_index.IndexConnectionError = IndexConnectionError
mock_index.QueryError = QueryError
# def _raiseIndexConnectionError(*args, **kwargs):
# raise IndexConnectionError('What now')
mock_index.get_document.side_effect = IndexConnectionError
with self.assertRaises(InternalServerError):
response_data, code, headers = simple.retrieve_document('124.5678')
self.assertEqual(mock_index.get_document.call_count, 1,
"A search should be attempted")
call_args, call_kwargs = mock_index.get_document.call_args
self.assertIsInstance(call_args[0], str, "arXiv ID is passed")
# self.assertEqual(code, status.HTTP_500_INTERNAL_SERVER_ERROR)
[docs] @mock.patch('search.controllers.simple.index')
def test_document_not_found(self, mock_index):
"""The document is not found."""
# We need to explicit assign the exception to the mock, otherwise the
# exception raised in the side-effect will just be a mock object (not
# inheriting from BaseException).
mock_index.QueryError = QueryError
mock_index.IndexConnectionError = IndexConnectionError
mock_index.DocumentNotFound = DocumentNotFound
def _raiseDocumentNotFound(*args, **kwargs):
raise DocumentNotFound('What now')
mock_index.get_document.side_effect = _raiseDocumentNotFound
with self.assertRaises(NotFound):
try:
response_data, code, headers = simple.retrieve_document(1)
except DocumentNotFound as e:
self.fail("DocumentNotFound should be handled (caught %s)" % e)
self.assertEqual(mock_index.get_document.call_count, 1,
"A search should be attempted")
[docs]class TestSearchController(TestCase):
"""Tests for :func:`.simple.search`."""
[docs] @mock.patch('search.controllers.simple.url_for',
lambda *a, **k: f'https://arxiv.org/{k["paper_id"]}')
@mock.patch('search.controllers.simple.index')
def test_arxiv_id(self, mock_index):
"""Query parameter contains an arXiv ID."""
request_data = MultiDict({'query': '1702.00123'})
response_data, code, headers = simple.search(request_data)
self.assertEqual(code, status.HTTP_301_MOVED_PERMANENTLY,
"Response should be a 301 redirect.")
self.assertIn('Location', headers, "Location header should be set")
self.assertEqual(mock_index.search.call_count, 0,
"No search should be attempted")
[docs] @mock.patch('search.controllers.simple.index')
def test_single_field_term(self, mock_index):
"""Form data are present."""
mock_index.search.return_value = DocumentSet(metadata={}, results=[])
request_data = MultiDict({
'searchtype': 'title',
'query': 'foo title'
})
response_data, code, headers = simple.search(request_data)
self.assertEqual(mock_index.search.call_count, 1,
"A search should be attempted")
call_args, call_kwargs = mock_index.search.call_args
self.assertIsInstance(call_args[0], SimpleQuery,
"An SimpleQuery is passed to the search index")
self.assertEqual(code, status.HTTP_200_OK, "Response should be OK.")
[docs] @mock.patch('search.controllers.simple.index')
def test_invalid_data(self, mock_index):
"""Form data are invalid."""
request_data = MultiDict({
'searchtype': 'title'
})
response_data, code, headers = simple.search(request_data)
self.assertEqual(code, status.HTTP_200_OK, "Response should be OK.")
self.assertIn('form', response_data, "Response should include form.")
self.assertEqual(mock_index.search.call_count, 0,
"No search should be attempted")
[docs] @mock.patch('search.controllers.simple.index')
def test_index_raises_connection_exception(self, mock_index):
"""Index service raises a IndexConnectionError."""
# We need to explicit assign the exception to the mock, otherwise the
# exception raised in the side-effect will just be a mock object (not
# inheriting from BaseException).
mock_index.IndexConnectionError = IndexConnectionError
def _raiseIndexConnectionError(*args, **kwargs):
raise IndexConnectionError('What now')
mock_index.search.side_effect = _raiseIndexConnectionError
request_data = MultiDict({
'searchtype': 'title',
'query': 'foo title'
})
with self.assertRaises(InternalServerError):
response_data, code, headers = simple.search(request_data)
self.assertEqual(mock_index.search.call_count, 1,
"A search should be attempted")
call_args, call_kwargs = mock_index.search.call_args
self.assertIsInstance(call_args[0], SimpleQuery,
"An SimpleQuery is passed to the search index")
[docs] @mock.patch('search.controllers.simple.index')
def test_index_raises_query_error(self, mock_index):
"""Index service raises a QueryError."""
# We need to explicit assign the exception to the mock, otherwise the
# exception raised in the side-effect will just be a mock object (not
# inheriting from BaseException).
mock_index.QueryError = QueryError
mock_index.IndexConnectionError = IndexConnectionError
def _raiseQueryError(*args, **kwargs):
raise QueryError('What now')
mock_index.search.side_effect = _raiseQueryError
request_data = MultiDict({
'searchtype': 'title',
'query': 'foo title'
})
with self.assertRaises(InternalServerError):
try:
response_data, code, headers = simple.search(request_data)
except QueryError as e:
self.fail("QueryError should be handled (caught %s)" % e)
self.assertEqual(mock_index.search.call_count, 1,
"A search should be attempted")
[docs]class TestClassicAuthorSyntaxIsIntercepted(TestCase):
"""
The user may have entered an author query using `surname_f` syntax.
This is an artefact of the classic search system, and not intended to be
supported. Nevertheless, users have become accustomed to this syntax. We
therefore rewrite the query using a comma, and show the user a warning
about the syntax change.
"""
[docs] @mock.patch('search.controllers.simple.index')
def test_all_fields_search_contains_classic_syntax(self, mock_index):
"""User has entered a `surname_f` query in an all-fields search."""
request_data = MultiDict({
'searchtype': 'all',
'query': 'franklin_r',
'size': 50,
'order': ''
})
mock_index.search.return_value = DocumentSet(metadata={}, results=[])
data, code, headers = simple.search(request_data)
self.assertEqual(data['query'].value, "franklin, r",
"The query should be rewritten.")
self.assertTrue(data['has_classic_format'],
"A flag denoting the syntax interception should be set"
" in the response context, so that a message may be"
" rendered in the template.")
[docs] @mock.patch('search.controllers.simple.index')
def test_author_search_contains_classic_syntax(self, mock_index):
"""User has entered a `surname_f` query in an author search."""
request_data = MultiDict({
'searchtype': 'author',
'query': 'franklin_r',
'size': 50,
'order': ''
})
mock_index.search.return_value = DocumentSet(metadata={}, results=[])
data, code, headers = simple.search(request_data)
self.assertEqual(data['query'].value, "franklin, r",
"The query should be rewritten.")
self.assertTrue(data['has_classic_format'],
"A flag denoting the syntax interception should be set"
" in the response context, so that a message may be"
" rendered in the template.")
[docs] @mock.patch('search.controllers.simple.index')
def test_all_fields_search_multiple_classic_syntax(self, mock_index):
"""User has entered a classic query with multiple authors."""
request_data = MultiDict({
'searchtype': 'all',
'query': 'j franklin_r hawking_s',
'size': 50,
'order': ''
})
mock_index.search.return_value = DocumentSet(metadata={}, results=[])
data, code, headers = simple.search(request_data)
self.assertEqual(data['query'].value, "j franklin, r; hawking, s",
"The query should be rewritten.")
self.assertTrue(data['has_classic_format'],
"A flag denoting the syntax interception should be set"
" in the response context, so that a message may be"
" rendered in the template.")
[docs] @mock.patch('search.controllers.simple.index')
def test_title_search_contains_classic_syntax(self, mock_index):
"""User has entered a `surname_f` query in a title search."""
request_data = MultiDict({
'searchtype': 'title',
'query': 'franklin_r',
'size': 50,
'order': ''
})
mock_index.search.return_value = DocumentSet(metadata={}, results=[])
data, code, headers = simple.search(request_data)
self.assertEqual(data['query'].value, "franklin_r",
"The query should not be rewritten.")
self.assertFalse(data['has_classic_format'],
"Flag should not be set, as no rewrite has occurred.")