first commit

This commit is contained in:
Iyas Altawil
2025-06-26 15:38:10 +03:30
commit e928faf6d2
899 changed files with 403713 additions and 0 deletions

92
tests/__init__.py Normal file
View File

@@ -0,0 +1,92 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
import pathlib
import os
import aiounittest
os.environ.pop('SEARXNG_SETTINGS_PATH', None)
os.environ['SEARXNG_DISABLE_ETC_SETTINGS'] = '1'
class SearxTestLayer:
"""Base layer for non-robot tests."""
__name__ = 'SearxTestLayer'
@classmethod
def setUp(cls):
pass
@classmethod
def tearDown(cls):
pass
@classmethod
def testSetUp(cls):
pass
@classmethod
def testTearDown(cls):
pass
class SearxTestCase(aiounittest.AsyncTestCase):
"""Base test case for non-robot tests."""
layer = SearxTestLayer
SETTINGS_FOLDER = pathlib.Path(__file__).parent / "unit" / "settings"
TEST_SETTINGS = "test_settings.yml"
def setUp(self):
self.init_test_settings()
def setattr4test(self, obj, attr, value):
"""setattr(obj, attr, value) but reset to the previous value in the
cleanup."""
previous_value = getattr(obj, attr)
def cleanup_patch():
setattr(obj, attr, previous_value)
self.addCleanup(cleanup_patch)
setattr(obj, attr, value)
def init_test_settings(self):
"""Sets ``SEARXNG_SETTINGS_PATH`` environment variable an initialize
global ``settings`` variable and the ``logger`` from a test config in
:origin:`tests/unit/settings/`.
"""
os.environ['SEARXNG_SETTINGS_PATH'] = str(self.SETTINGS_FOLDER / self.TEST_SETTINGS)
# pylint: disable=import-outside-toplevel
import searx
import searx.locales
import searx.plugins
import searx.search
import searx.webapp
# https://flask.palletsprojects.com/en/stable/config/#builtin-configuration-values
# searx.webapp.app.config["DEBUG"] = True
searx.webapp.app.config["TESTING"] = True # to get better error messages
searx.webapp.app.config["EXPLAIN_TEMPLATE_LOADING"] = True
searx.init_settings()
searx.plugins.initialize(searx.webapp.app)
# searx.search.initialize will:
# - load the engines and
# - initialize searx.network, searx.metrics, searx.processors and searx.search.checker
searx.search.initialize(
enable_checker=True,
check_network=True,
enable_metrics=searx.get_setting("general.enable_metrics"), # type: ignore
)
# pylint: disable=attribute-defined-outside-init
self.app = searx.webapp.app
self.client = searx.webapp.app.test_client()

2
tests/robot/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name

69
tests/robot/__main__.py Normal file
View File

@@ -0,0 +1,69 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
"""Shared testing code."""
import sys
import os
import subprocess
import traceback
import pathlib
import shutil
from splinter import Browser
import tests as searx_tests
from tests.robot import test_webapp
class SearxRobotLayer:
"""Searx Robot Test Layer"""
def setUp(self):
os.setpgrp() # create new process group, become its leader
tests_path = pathlib.Path(searx_tests.__file__).resolve().parent
# get program paths
webapp = str(tests_path.parent / 'searx' / 'webapp.py')
exe = 'python'
# set robot settings path
os.environ['SEARXNG_SETTINGS_PATH'] = str(tests_path / 'robot' / 'settings_robot.yml')
# run the server
self.server = subprocess.Popen( # pylint: disable=consider-using-with
[exe, webapp], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
if hasattr(self.server.stdout, 'read1'):
print(self.server.stdout.read1(1024).decode())
def tearDown(self):
os.kill(self.server.pid, 9)
# remove previously set environment variable
del os.environ['SEARXNG_SETTINGS_PATH']
def run_robot_tests(tests):
print('Running {0} tests'.format(len(tests)))
print(f'{shutil.which("geckodriver")}')
print(f'{shutil.which("firefox")}')
for test in tests:
with Browser('firefox', headless=True, profile_preferences={'intl.accept_languages': 'en'}) as browser:
test(browser)
def main():
test_layer = SearxRobotLayer()
try:
test_layer.setUp()
run_robot_tests([getattr(test_webapp, x) for x in dir(test_webapp) if x.startswith('test_')])
except Exception: # pylint: disable=broad-except
print('Error occurred: {0}'.format(traceback.format_exc()))
sys.exit(1)
finally:
test_layer.tearDown()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,58 @@
general:
debug: false
instance_name: "searx_test"
brand:
git_url: https://github.com/searxng/searxng
git_branch: master
issue_url: https://github.com/searxng/searxng/issues
new_issue_url: https://github.com/searxng/searxng/issues/new
docs_url: https://docs.searxng.org
public_instances: https://searx.space
wiki_url: https://github.com/searxng/searxng/wiki
search:
language: "all"
server:
port: 11111
bind_address: 127.0.0.1
secret_key: "changedultrasecretkey"
base_url: false
http_protocol_version: "1.0"
ui:
static_path: ""
templates_path: ""
default_theme: simple
preferences:
lock: []
outgoing:
request_timeout: 1.0 # seconds
useragent_suffix: ""
categories_as_tabs:
general:
dummy:
engines:
- name: general dummy
engine: dummy
categories: general
shortcut: gd
- name: dummy dummy
engine: dummy
categories: dummy
shortcut: dd
doi_resolvers:
oadoi.org: 'https://oadoi.org/'
doi.org: 'https://doi.org/'
sci-hub.se: 'https://sci-hub.se/'
sci-hub.st: 'https://sci-hub.st/'
sci-hub.ru: 'https://sci-hub.ru/'
default_doi_resolver: 'oadoi.org'

View File

@@ -0,0 +1,77 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from time import sleep
url = "http://localhost:11111/"
def test_index(browser):
# Visit URL
browser.visit(url)
assert browser.is_text_present('SearXNG')
def test_404(browser):
# Visit URL
browser.visit(url + 'missing_link')
assert browser.is_text_present('Page not found')
def test_about(browser):
browser.visit(url)
browser.links.find_by_text('SearXNG').click()
assert browser.is_text_present('Why use it?')
def test_preferences(browser):
browser.visit(url)
browser.links.find_by_href('/preferences').click()
assert browser.is_text_present('Preferences')
assert browser.is_text_present('COOKIES')
assert browser.is_element_present_by_xpath('//label[@for="checkbox_dummy"]')
def test_preferences_engine_select(browser):
browser.visit(url)
browser.links.find_by_href('/preferences').click()
assert browser.is_element_present_by_xpath('//label[@for="tab-engines"]')
browser.find_by_xpath('//label[@for="tab-engines"]').first.click()
assert not browser.find_by_xpath('//input[@id="engine_general_dummy__general"]').first.checked
browser.find_by_xpath('//label[@for="engine_general_dummy__general"]').first.check()
browser.find_by_xpath('//input[@type="submit"]').first.click()
# waiting for the redirect - without this the test is flaky..
sleep(1)
browser.visit(url)
browser.links.find_by_href('/preferences').click()
browser.find_by_xpath('//label[@for="tab-engines"]').first.click()
assert browser.find_by_xpath('//input[@id="engine_general_dummy__general"]').first.checked
def test_preferences_locale(browser):
browser.visit(url)
browser.links.find_by_href('/preferences').click()
browser.find_by_xpath('//label[@for="tab-ui"]').first.click()
browser.select('locale', 'fr')
browser.find_by_xpath('//input[@type="submit"]').first.click()
# waiting for the redirect - without this the test is flaky..
sleep(1)
browser.visit(url)
browser.links.find_by_href('/preferences').click()
browser.is_text_present('Préférences')
def test_search(browser):
browser.visit(url)
browser.fill('q', 'test search query')
browser.find_by_xpath('//button[@type="submit"]').first.click()
assert browser.is_text_present('No results were found')

10
tests/unit/__init__.py Normal file
View File

@@ -0,0 +1,10 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
import os
from pathlib import Path
# By default, in unit tests the user settings from
# unit/settings/test_settings.yml are used.
os.environ['SEARXNG_SETTINGS_PATH'] = str(Path(__file__).parent / "settings" / "test_settings.yml")

View File

@@ -0,0 +1,2 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name

View File

@@ -0,0 +1,218 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from searx.engines import command as command_engine
from searx.result_types import KeyValue
from tests import SearxTestCase
class TestCommandEngine(SearxTestCase):
def test_basic_seq_command_engine(self):
ls_engine = command_engine
ls_engine.command = ['seq', '{{QUERY}}']
ls_engine.delimiter = {'chars': ' ', 'keys': ['number']}
expected_results = [
KeyValue(kvmap={'number': 1}),
KeyValue(kvmap={'number': 2}),
KeyValue(kvmap={'number': 3}),
KeyValue(kvmap={'number': 4}),
KeyValue(kvmap={'number': 5}),
]
results = ls_engine.search('5', {'pageno': 1})
for i, expected in enumerate(expected_results):
self.assertEqual(results[i].kvmap["number"], str(expected.kvmap["number"]))
def test_delimiter_parsing(self):
searx_logs = '''DEBUG:searx.webapp:static directory is /home/n/p/searx/searx/static
DEBUG:searx.webapp:templates directory is /home/n/p/searx/searx/templates
DEBUG:searx.engines:soundcloud engine: Starting background initialization
DEBUG:searx.engines:wolframalpha engine: Starting background initialization
DEBUG:searx.engines:locate engine: Starting background initialization
DEBUG:searx.engines:regex search in files engine: Starting background initialization
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): www.wolframalpha.com
DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): soundcloud.com
DEBUG:searx.engines:find engine: Starting background initialization
DEBUG:searx.engines:pattern search in files engine: Starting background initialization
DEBUG:searx.webapp:starting webserver on 127.0.0.1:8888
WARNING:werkzeug: * Debugger is active!
INFO:werkzeug: * Debugger PIN: 299-578-362'''
echo_engine = command_engine
echo_engine.command = ['echo', searx_logs]
echo_engine.delimiter = {'chars': ':', 'keys': ['level', 'component', 'message']}
page1 = [
{
'component': 'searx.webapp',
'message': 'static directory is /home/n/p/searx/searx/static',
'level': 'DEBUG',
},
{
'component': 'searx.webapp',
'message': 'templates directory is /home/n/p/searx/searx/templates',
'level': 'DEBUG',
},
{
'component': 'searx.engines',
'message': 'soundcloud engine: Starting background initialization',
'level': 'DEBUG',
},
{
'component': 'searx.engines',
'message': 'wolframalpha engine: Starting background initialization',
'level': 'DEBUG',
},
{
'component': 'searx.engines',
'message': 'locate engine: Starting background initialization',
'level': 'DEBUG',
},
{
'component': 'searx.engines',
'message': 'regex search in files engine: Starting background initialization',
'level': 'DEBUG',
},
{
'component': 'urllib3.connectionpool',
'message': 'Starting new HTTPS connection (1): www.wolframalpha.com',
'level': 'DEBUG',
},
{
'component': 'urllib3.connectionpool',
'message': 'Starting new HTTPS connection (1): soundcloud.com',
'level': 'DEBUG',
},
{
'component': 'searx.engines',
'message': 'find engine: Starting background initialization',
'level': 'DEBUG',
},
{
'component': 'searx.engines',
'message': 'pattern search in files engine: Starting background initialization',
'level': 'DEBUG',
},
]
page2 = [
{
'component': 'searx.webapp',
'message': 'starting webserver on 127.0.0.1:8888',
'level': 'DEBUG',
},
{
'component': 'werkzeug',
'message': ' * Debugger is active!',
'level': 'WARNING',
},
{
'component': 'werkzeug',
'message': ' * Debugger PIN: 299-578-362',
'level': 'INFO',
},
]
page1 = [KeyValue(kvmap=row) for row in page1]
page2 = [KeyValue(kvmap=row) for row in page2]
expected_results_by_page = [page1, page2]
for i in [0, 1]:
results = echo_engine.search('', {'pageno': i + 1})
page = expected_results_by_page[i]
for i, expected in enumerate(page):
self.assertEqual(expected.kvmap["message"], str(results[i].kvmap["message"]))
def test_regex_parsing(self):
txt = '''commit 35f9a8c81d162a361b826bbcd4a1081a4fbe76a7
Author: Noémi Ványi <sitbackandwait@gmail.com>
Date: Tue Oct 15 11:31:33 2019 +0200
first interesting message
commit 6c3c206316153ccc422755512bceaa9ab0b14faa
Author: Noémi Ványi <sitbackandwait@gmail.com>
Date: Mon Oct 14 17:10:08 2019 +0200
second interesting message
commit d8594d2689b4d5e0d2f80250223886c3a1805ef5
Author: Noémi Ványi <sitbackandwait@gmail.com>
Date: Mon Oct 14 14:45:05 2019 +0200
third interesting message
commit '''
git_log_engine = command_engine
git_log_engine.command = ['echo', txt]
git_log_engine.result_separator = '\n\ncommit '
git_log_engine.delimiter = {}
git_log_engine.parse_regex = {
'commit': r'\w{40}',
'author': r'[\w* ]* <\w*@?\w*\.?\w*>',
'date': r'Date: .*',
'message': r'\n\n.*$',
}
git_log_engine.init({"command": git_log_engine.command, "parse_regex": git_log_engine.parse_regex})
expected_results = [
{
'commit': '35f9a8c81d162a361b826bbcd4a1081a4fbe76a7',
'author': ' Noémi Ványi <sitbackandwait@gmail.com>',
'date': 'Date: Tue Oct 15 11:31:33 2019 +0200',
'message': '\n\nfirst interesting message',
},
{
'commit': '6c3c206316153ccc422755512bceaa9ab0b14faa',
'author': ' Noémi Ványi <sitbackandwait@gmail.com>',
'date': 'Date: Mon Oct 14 17:10:08 2019 +0200',
'message': '\n\nsecond interesting message',
},
{
'commit': 'd8594d2689b4d5e0d2f80250223886c3a1805ef5',
'author': ' Noémi Ványi <sitbackandwait@gmail.com>',
'date': 'Date: Mon Oct 14 14:45:05 2019 +0200',
'message': '\n\nthird interesting message',
},
]
expected_results = [KeyValue(kvmap=kvmap) for kvmap in expected_results]
results = git_log_engine.search('', {'pageno': 1})
for i, expected in enumerate(expected_results):
self.assertEqual(expected.kvmap["message"], str(results[i].kvmap["message"]))
def test_working_dir_path_query(self):
ls_engine = command_engine
ls_engine.command = ['ls', '{{QUERY}}']
ls_engine.result_separator = '\n'
ls_engine.delimiter = {'chars': ' ', 'keys': ['file']}
ls_engine.query_type = 'path'
results = ls_engine.search('.', {'pageno': 1})
self.assertTrue(len(results) != 0)
forbidden_paths = [
'..',
'../..',
'./..',
'~',
'/var',
]
for forbidden_path in forbidden_paths:
self.assertRaises(ValueError, ls_engine.search, forbidden_path, {'pageno': 1})
def test_enum_queries(self):
echo_engine = command_engine
echo_engine.command = ['echo', '{{QUERY}}']
echo_engine.query_type = 'enum'
echo_engine.query_enum = ['i-am-allowed-to-say-this', 'and-that']
for allowed in echo_engine.query_enum:
results = echo_engine.search(allowed, {'pageno': 1})
self.assertTrue(len(results) != 0)
forbidden_queries = [
'forbidden',
'banned',
'prohibited',
]
for forbidden in forbidden_queries:
self.assertRaises(ValueError, echo_engine.search, forbidden, {'pageno': 1})

View File

@@ -0,0 +1,254 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring
from collections import defaultdict
import mock
from searx.engines import json_engine
from searx import logger
from tests import SearxTestCase
logger = logger.getChild('engines')
class TestJsonEngine(SearxTestCase): # pylint: disable=missing-class-docstring
json = """
[
{
"title": "title0",
"content": "content0",
"url": "https://example.com/url0",
"images": [
{
"thumb": "https://example.com/thumb00"
},
{
"thumb": "https://example.com/thumb01"
}
]
},
{
"title": "<h1>title1</h1>",
"content": "<h2>content1</h2>",
"url": "https://example.com/url1",
"images": [
{
"thumb": "https://example.com/thumb10"
},
{
"thumb": "https://example.com/thumb11"
}
]
},
{
"title": "title2",
"content": "content2",
"url": 2,
"images": [
{
"thumb": "thumb20"
},
{
"thumb": 21
}
]
}
]
"""
json_result_query = """
{
"data": {
"results": [
{
"title": "title0",
"content": "content0",
"url": "https://example.com/url0",
"images": [
{
"thumb": "https://example.com/thumb00"
},
{
"thumb": "https://example.com/thumb01"
}
]
},
{
"title": "<h1>title1</h1>",
"content": "<h2>content1</h2>",
"url": "https://example.com/url1",
"images": [
{
"thumb": "https://example.com/thumb10"
},
{
"thumb": "https://example.com/thumb11"
}
]
},
{
"title": "title2",
"content": "content2",
"url": 2,
"images": [
{
"thumb": "thumb20"
},
{
"thumb": 21
}
]
}
],
"suggestions": [
"suggestion0",
"suggestion1"
]
}
}
"""
def setUp(self):
json_engine.logger = logger.getChild('test_json_engine')
def test_request(self):
json_engine.search_url = 'https://example.com/{query}'
json_engine.categories = []
json_engine.paging = False
query = 'test_query'
dicto = defaultdict(dict)
dicto['language'] = 'all'
dicto['pageno'] = 1
params = json_engine.request(query, dicto)
self.assertIn('url', params)
self.assertEqual('https://example.com/test_query', params['url'])
json_engine.search_url = 'https://example.com/q={query}&p={pageno}'
json_engine.paging = True
query = 'test_query'
dicto = defaultdict(dict)
dicto['language'] = 'all'
dicto['pageno'] = 1
params = json_engine.request(query, dicto)
self.assertIn('url', params)
self.assertEqual('https://example.com/q=test_query&p=1', params['url'])
json_engine.search_url = 'https://example.com/'
json_engine.paging = True
json_engine.request_body = '{{"page": {pageno}, "query": "{query}"}}'
query = 'test_query'
dicto = defaultdict(dict)
dicto['language'] = 'all'
dicto['pageno'] = 1
params = json_engine.request(query, dicto)
self.assertIn('data', params)
self.assertEqual('{"page": 1, "query": "test_query"}', params['data'])
def test_response(self):
# without results_query
json_engine.results_query = ''
json_engine.url_query = 'url'
json_engine.url_prefix = ''
json_engine.title_query = 'title'
json_engine.content_query = 'content'
json_engine.thumbnail_query = 'images/thumb'
json_engine.thumbnail_prefix = ''
json_engine.title_html_to_text = False
json_engine.content_html_to_text = False
json_engine.categories = []
self.assertRaises(AttributeError, json_engine.response, None)
self.assertRaises(AttributeError, json_engine.response, [])
self.assertRaises(AttributeError, json_engine.response, '')
self.assertRaises(AttributeError, json_engine.response, '[]')
response = mock.Mock(text='{}', status_code=200)
self.assertEqual(json_engine.response(response), [])
response = mock.Mock(text=self.json, status_code=200)
results = json_engine.response(response)
self.assertEqual(type(results), list)
self.assertEqual(len(results), 3)
self.assertEqual(results[0]['title'], 'title0')
self.assertEqual(results[0]['url'], 'https://example.com/url0')
self.assertEqual(results[0]['content'], 'content0')
self.assertEqual(results[0]['thumbnail'], 'https://example.com/thumb00')
self.assertEqual(results[1]['title'], '<h1>title1</h1>')
self.assertEqual(results[1]['url'], 'https://example.com/url1')
self.assertEqual(results[1]['content'], '<h2>content1</h2>')
self.assertEqual(results[1]['thumbnail'], 'https://example.com/thumb10')
# with prefix and suggestions without results_query
json_engine.url_prefix = 'https://example.com/url'
json_engine.thumbnail_query = 'images/1/thumb'
json_engine.thumbnail_prefix = 'https://example.com/thumb'
results = json_engine.response(response)
self.assertEqual(type(results), list)
self.assertEqual(len(results), 3)
self.assertEqual(results[2]['title'], 'title2')
self.assertEqual(results[2]['url'], 'https://example.com/url2')
self.assertEqual(results[2]['content'], 'content2')
self.assertEqual(results[2]['thumbnail'], 'https://example.com/thumb21')
self.assertFalse(results[0].get('is_onion', False))
# results are onion urls without results_query
json_engine.categories = ['onions']
results = json_engine.response(response)
self.assertTrue(results[0]['is_onion'])
def test_response_results_json(self):
# with results_query
json_engine.results_query = 'data/results'
json_engine.url_query = 'url'
json_engine.url_prefix = ''
json_engine.title_query = 'title'
json_engine.content_query = 'content'
json_engine.thumbnail_query = 'images/1/thumb'
json_engine.thumbnail_prefix = ''
json_engine.title_html_to_text = True
json_engine.content_html_to_text = True
json_engine.categories = []
self.assertRaises(AttributeError, json_engine.response, None)
self.assertRaises(AttributeError, json_engine.response, [])
self.assertRaises(AttributeError, json_engine.response, '')
self.assertRaises(AttributeError, json_engine.response, '[]')
response = mock.Mock(text='{}', status_code=200)
self.assertEqual(json_engine.response(response), [])
response = mock.Mock(text=self.json_result_query, status_code=200)
results = json_engine.response(response)
self.assertEqual(type(results), list)
self.assertEqual(len(results), 3)
self.assertEqual(results[0]['title'], 'title0')
self.assertEqual(results[0]['url'], 'https://example.com/url0')
self.assertEqual(results[0]['content'], 'content0')
self.assertEqual(results[0]['thumbnail'], 'https://example.com/thumb01')
self.assertEqual(results[1]['title'], 'title1')
self.assertEqual(results[1]['url'], 'https://example.com/url1')
self.assertEqual(results[1]['content'], 'content1')
self.assertEqual(results[1]['thumbnail'], 'https://example.com/thumb11')
# with prefix and suggestions with results_query
json_engine.url_prefix = 'https://example.com/url'
json_engine.thumbnail_query = 'images/1/thumb'
json_engine.thumbnail_prefix = 'https://example.com/thumb'
json_engine.suggestion_query = 'data/suggestions'
results = json_engine.response(response)
self.assertEqual(type(results), list)
self.assertEqual(len(results), 4)
self.assertEqual(results[2]['title'], 'title2')
self.assertEqual(results[2]['url'], 'https://example.com/url2')
self.assertEqual(results[2]['content'], 'content2')
self.assertEqual(results[2]['thumbnail'], 'https://example.com/thumb21')
self.assertEqual(results[3]['suggestion'], ['suggestion0', 'suggestion1'])
self.assertFalse(results[0].get('is_onion', False))
# results are onion urls with results_query
json_engine.categories = ['onions']
results = json_engine.response(response)
self.assertTrue(results[0]['is_onion'])

View File

@@ -0,0 +1,136 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from collections import defaultdict
import mock
from searx.engines import xpath
from searx import logger
from tests import SearxTestCase
logger = logger.getChild('engines')
class TestXpathEngine(SearxTestCase):
html = """
<div>
<div class="search_result">
<a class="result" href="https://result1.com">Result 1</a>
<p class="content">Content 1</p>
<a class="cached" href="https://cachedresult1.com">Cache</a>
</div>
<div class="search_result">
<a class="result" href="https://result2.com">Result 2</a>
<p class="content">Content 2</p>
<a class="cached" href="https://cachedresult2.com">Cache</a>
</div>
</div>
"""
def setUp(self):
super().setUp()
xpath.logger = logger.getChild('test_xpath')
def test_request(self):
xpath.search_url = 'https://url.com/{query}'
xpath.categories = []
xpath.paging = False
query = 'test_query'
dicto = defaultdict(dict)
dicto['language'] = 'all'
dicto['pageno'] = 1
params = xpath.request(query, dicto)
self.assertIn('url', params)
self.assertEqual('https://url.com/test_query', params['url'])
xpath.search_url = 'https://url.com/q={query}&p={pageno}'
xpath.paging = True
query = 'test_query'
dicto = defaultdict(dict)
dicto['language'] = 'all'
dicto['pageno'] = 1
params = xpath.request(query, dicto)
self.assertIn('url', params)
self.assertEqual('https://url.com/q=test_query&p=1', params['url'])
def test_response(self):
# without results_xpath
xpath.url_xpath = '//div[@class="search_result"]//a[@class="result"]/@href'
xpath.title_xpath = '//div[@class="search_result"]//a[@class="result"]'
xpath.content_xpath = '//div[@class="search_result"]//p[@class="content"]'
self.assertRaises(AttributeError, xpath.response, None)
self.assertRaises(AttributeError, xpath.response, [])
self.assertRaises(AttributeError, xpath.response, '')
self.assertRaises(AttributeError, xpath.response, '[]')
response = mock.Mock(text='<html></html>', status_code=200)
self.assertEqual(xpath.response(response), [])
response = mock.Mock(text=self.html, status_code=200)
results = xpath.response(response)
self.assertIsInstance(results, list)
self.assertEqual(len(results), 2)
self.assertEqual(results[0]['title'], 'Result 1')
self.assertEqual(results[0]['url'], 'https://result1.com/')
self.assertEqual(results[0]['content'], 'Content 1')
self.assertEqual(results[1]['title'], 'Result 2')
self.assertEqual(results[1]['url'], 'https://result2.com/')
self.assertEqual(results[1]['content'], 'Content 2')
# with cached urls, without results_xpath
xpath.cached_xpath = '//div[@class="search_result"]//a[@class="cached"]/@href'
results = xpath.response(response)
self.assertIsInstance(results, list)
self.assertEqual(len(results), 2)
self.assertEqual(results[0]['cached_url'], 'https://cachedresult1.com')
self.assertEqual(results[1]['cached_url'], 'https://cachedresult2.com')
self.assertFalse(results[0].get('is_onion', False))
# results are onion urls (no results_xpath)
xpath.categories = ['onions']
results = xpath.response(response)
self.assertTrue(results[0]['is_onion'])
def test_response_results_xpath(self):
# with results_xpath
xpath.results_xpath = '//div[@class="search_result"]'
xpath.url_xpath = './/a[@class="result"]/@href'
xpath.title_xpath = './/a[@class="result"]'
xpath.content_xpath = './/p[@class="content"]'
xpath.cached_xpath = None
xpath.categories = []
self.assertRaises(AttributeError, xpath.response, None)
self.assertRaises(AttributeError, xpath.response, [])
self.assertRaises(AttributeError, xpath.response, '')
self.assertRaises(AttributeError, xpath.response, '[]')
response = mock.Mock(text='<html></html>', status_code=200)
self.assertEqual(xpath.response(response), [])
response = mock.Mock(text=self.html, status_code=200)
results = xpath.response(response)
self.assertIsInstance(results, list)
self.assertEqual(len(results), 2)
self.assertEqual(results[0]['title'], 'Result 1')
self.assertEqual(results[0]['url'], 'https://result1.com/')
self.assertEqual(results[0]['content'], 'Content 1')
self.assertEqual(results[1]['title'], 'Result 2')
self.assertEqual(results[1]['url'], 'https://result2.com/')
self.assertEqual(results[1]['content'], 'Content 2')
# with cached urls, with results_xpath
xpath.cached_xpath = './/a[@class="cached"]/@href'
results = xpath.response(response)
self.assertIsInstance(results, list)
self.assertEqual(len(results), 2)
self.assertEqual(results[0]['cached_url'], 'https://cachedresult1.com')
self.assertEqual(results[1]['cached_url'], 'https://cachedresult2.com')
self.assertFalse(results[0].get('is_onion', False))
# results are onion urls (with results_xpath)
xpath.categories = ['onions']
results = xpath.response(response)
self.assertTrue(results[0]['is_onion'])

View File

@@ -0,0 +1,2 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name

View File

@@ -0,0 +1,247 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
import httpx
from mock import patch
from searx.network.network import Network, NETWORKS
from tests import SearxTestCase
class TestNetwork(SearxTestCase):
# pylint: disable=protected-access
def test_simple(self):
network = Network()
self.assertEqual(next(network._local_addresses_cycle), None)
self.assertEqual(next(network._proxies_cycle), ())
def test_ipaddress_cycle(self):
network = NETWORKS['ipv6']
self.assertEqual(next(network._local_addresses_cycle), '::')
self.assertEqual(next(network._local_addresses_cycle), '::')
network = NETWORKS['ipv4']
self.assertEqual(next(network._local_addresses_cycle), '0.0.0.0')
self.assertEqual(next(network._local_addresses_cycle), '0.0.0.0')
network = Network(local_addresses=['192.168.0.1', '192.168.0.2'])
self.assertEqual(next(network._local_addresses_cycle), '192.168.0.1')
self.assertEqual(next(network._local_addresses_cycle), '192.168.0.2')
self.assertEqual(next(network._local_addresses_cycle), '192.168.0.1')
network = Network(local_addresses=['192.168.0.0/30'])
self.assertEqual(next(network._local_addresses_cycle), '192.168.0.1')
self.assertEqual(next(network._local_addresses_cycle), '192.168.0.2')
self.assertEqual(next(network._local_addresses_cycle), '192.168.0.1')
self.assertEqual(next(network._local_addresses_cycle), '192.168.0.2')
network = Network(local_addresses=['fe80::/10'])
self.assertEqual(next(network._local_addresses_cycle), 'fe80::1')
self.assertEqual(next(network._local_addresses_cycle), 'fe80::2')
self.assertEqual(next(network._local_addresses_cycle), 'fe80::3')
with self.assertRaises(ValueError):
Network(local_addresses=['not_an_ip_address'])
def test_proxy_cycles(self):
network = Network(proxies='http://localhost:1337')
self.assertEqual(next(network._proxies_cycle), (('all://', 'http://localhost:1337'),))
network = Network(proxies={'https': 'http://localhost:1337', 'http': 'http://localhost:1338'})
self.assertEqual(
next(network._proxies_cycle), (('https://', 'http://localhost:1337'), ('http://', 'http://localhost:1338'))
)
self.assertEqual(
next(network._proxies_cycle), (('https://', 'http://localhost:1337'), ('http://', 'http://localhost:1338'))
)
network = Network(
proxies={'https': ['http://localhost:1337', 'http://localhost:1339'], 'http': 'http://localhost:1338'}
)
self.assertEqual(
next(network._proxies_cycle), (('https://', 'http://localhost:1337'), ('http://', 'http://localhost:1338'))
)
self.assertEqual(
next(network._proxies_cycle), (('https://', 'http://localhost:1339'), ('http://', 'http://localhost:1338'))
)
with self.assertRaises(ValueError):
Network(proxies=1)
def test_get_kwargs_clients(self):
kwargs = {
'verify': True,
'max_redirects': 5,
'timeout': 2,
'allow_redirects': True,
}
kwargs_client = Network.extract_kwargs_clients(kwargs)
self.assertEqual(len(kwargs_client), 2)
self.assertEqual(len(kwargs), 2)
self.assertEqual(kwargs['timeout'], 2)
self.assertEqual(kwargs['follow_redirects'], True)
self.assertTrue(kwargs_client['verify'])
self.assertEqual(kwargs_client['max_redirects'], 5)
async def test_get_client(self):
network = Network(verify=True)
client1 = await network.get_client()
client2 = await network.get_client(verify=True)
client3 = await network.get_client(max_redirects=10)
client4 = await network.get_client(verify=True)
client5 = await network.get_client(verify=False)
client6 = await network.get_client(max_redirects=10)
self.assertEqual(client1, client2)
self.assertEqual(client1, client4)
self.assertNotEqual(client1, client3)
self.assertNotEqual(client1, client5)
self.assertEqual(client3, client6)
await network.aclose()
async def test_aclose(self):
network = Network(verify=True)
await network.get_client()
await network.aclose()
async def test_request(self):
a_text = 'Lorem Ipsum'
response = httpx.Response(status_code=200, text=a_text)
with patch.object(httpx.AsyncClient, 'request', return_value=response):
network = Network(enable_http=True)
response = await network.request('GET', 'https://example.com/')
self.assertEqual(response.text, a_text)
await network.aclose()
class TestNetworkRequestRetries(SearxTestCase):
TEXT = 'Lorem Ipsum'
def setUp(self):
self.init_test_settings()
@classmethod
def get_response_404_then_200(cls):
first = True
async def get_response(*args, **kwargs): # pylint: disable=unused-argument
nonlocal first
if first:
first = False
return httpx.Response(status_code=403, text=TestNetworkRequestRetries.TEXT)
return httpx.Response(status_code=200, text=TestNetworkRequestRetries.TEXT)
return get_response
async def test_retries_ok(self):
with patch.object(httpx.AsyncClient, 'request', new=TestNetworkRequestRetries.get_response_404_then_200()):
network = Network(enable_http=True, retries=1, retry_on_http_error=403)
response = await network.request('GET', 'https://example.com/', raise_for_httperror=False)
self.assertEqual(response.text, TestNetworkRequestRetries.TEXT)
await network.aclose()
async def test_retries_fail_int(self):
with patch.object(httpx.AsyncClient, 'request', new=TestNetworkRequestRetries.get_response_404_then_200()):
network = Network(enable_http=True, retries=0, retry_on_http_error=403)
response = await network.request('GET', 'https://example.com/', raise_for_httperror=False)
self.assertEqual(response.status_code, 403)
await network.aclose()
async def test_retries_fail_list(self):
with patch.object(httpx.AsyncClient, 'request', new=TestNetworkRequestRetries.get_response_404_then_200()):
network = Network(enable_http=True, retries=0, retry_on_http_error=[403, 429])
response = await network.request('GET', 'https://example.com/', raise_for_httperror=False)
self.assertEqual(response.status_code, 403)
await network.aclose()
async def test_retries_fail_bool(self):
with patch.object(httpx.AsyncClient, 'request', new=TestNetworkRequestRetries.get_response_404_then_200()):
network = Network(enable_http=True, retries=0, retry_on_http_error=True)
response = await network.request('GET', 'https://example.com/', raise_for_httperror=False)
self.assertEqual(response.status_code, 403)
await network.aclose()
async def test_retries_exception_then_200(self):
request_count = 0
async def get_response(*args, **kwargs): # pylint: disable=unused-argument
nonlocal request_count
request_count += 1
if request_count < 3:
raise httpx.RequestError('fake exception', request=None)
return httpx.Response(status_code=200, text=TestNetworkRequestRetries.TEXT)
with patch.object(httpx.AsyncClient, 'request', new=get_response):
network = Network(enable_http=True, retries=2)
response = await network.request('GET', 'https://example.com/', raise_for_httperror=False)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.text, TestNetworkRequestRetries.TEXT)
await network.aclose()
async def test_retries_exception(self):
async def get_response(*args, **kwargs):
raise httpx.RequestError('fake exception', request=None)
with patch.object(httpx.AsyncClient, 'request', new=get_response):
network = Network(enable_http=True, retries=0)
with self.assertRaises(httpx.RequestError):
await network.request('GET', 'https://example.com/', raise_for_httperror=False)
await network.aclose()
class TestNetworkStreamRetries(SearxTestCase):
TEXT = 'Lorem Ipsum'
def setUp(self):
self.init_test_settings()
@classmethod
def get_response_exception_then_200(cls):
first = True
def stream(*args, **kwargs): # pylint: disable=unused-argument
nonlocal first
if first:
first = False
raise httpx.RequestError('fake exception', request=None)
return httpx.Response(status_code=200, text=TestNetworkStreamRetries.TEXT)
return stream
async def test_retries_ok(self):
with patch.object(httpx.AsyncClient, 'stream', new=TestNetworkStreamRetries.get_response_exception_then_200()):
network = Network(enable_http=True, retries=1, retry_on_http_error=403)
response = await network.stream('GET', 'https://example.com/')
self.assertEqual(response.text, TestNetworkStreamRetries.TEXT)
await network.aclose()
async def test_retries_fail(self):
with patch.object(httpx.AsyncClient, 'stream', new=TestNetworkStreamRetries.get_response_exception_then_200()):
network = Network(enable_http=True, retries=0, retry_on_http_error=403)
with self.assertRaises(httpx.RequestError):
await network.stream('GET', 'https://example.com/')
await network.aclose()
async def test_retries_exception(self):
first = True
def stream(*args, **kwargs): # pylint: disable=unused-argument
nonlocal first
if first:
first = False
return httpx.Response(status_code=403, text=TestNetworkRequestRetries.TEXT)
return httpx.Response(status_code=200, text=TestNetworkRequestRetries.TEXT)
with patch.object(httpx.AsyncClient, 'stream', new=stream):
network = Network(enable_http=True, retries=0, retry_on_http_error=403)
response = await network.stream('GET', 'https://example.com/', raise_for_httperror=False)
self.assertEqual(response.status_code, 403)
await network.aclose()

View File

@@ -0,0 +1,2 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name

View File

@@ -0,0 +1,38 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from searx.search import SearchQuery, EngineRef
from searx.search.processors import online
from searx import engines
from tests import SearxTestCase
TEST_ENGINE_NAME = "dummy engine" # from the ./settings/test_settings.yml
class TestOnlineProcessor(SearxTestCase):
def _get_params(self, online_processor, search_query, engine_category):
params = online_processor.get_params(search_query, engine_category)
self.assertIsNotNone(params)
assert params is not None
return params
def test_get_params_default_params(self):
engine = engines.engines[TEST_ENGINE_NAME]
online_processor = online.OnlineProcessor(engine, TEST_ENGINE_NAME)
search_query = SearchQuery('test', [EngineRef(TEST_ENGINE_NAME, 'general')], 'all', 0, 1, None, None, None)
params = self._get_params(online_processor, search_query, 'general')
self.assertIn('method', params)
self.assertIn('headers', params)
self.assertIn('data', params)
self.assertIn('url', params)
self.assertIn('cookies', params)
self.assertIn('auth', params)
def test_get_params_useragent(self):
engine = engines.engines[TEST_ENGINE_NAME]
online_processor = online.OnlineProcessor(engine, TEST_ENGINE_NAME)
search_query = SearchQuery('test', [EngineRef(TEST_ENGINE_NAME, 'general')], 'all', 0, 1, None, None, None)
params = self._get_params(online_processor, search_query, 'general')
self.assertIn('User-Agent', params['headers'])

View File

View File

@@ -0,0 +1,2 @@
[botdetection.ip_limit]
link_token = true

View File

@@ -0,0 +1,3 @@
Test:
"**********"
xxx

View File

@@ -0,0 +1,8 @@
# This SearXNG setup is used in unit tests
use_default_settings:
engines:
keep_only:
- google
- duckduckgo

View File

@@ -0,0 +1,30 @@
# This SearXNG setup is used in unit tests
use_default_settings:
engines:
# remove all engines
keep_only: []
search:
formats: [html, csv, json, rss]
server:
secret_key: "user_secret_key"
engines:
- name: dummy engine
engine: demo_offline
categories: ["general"]
shortcut: "gd"
timeout: 3
- name: dummy private engine
engine: demo_offline
categories: ["general"]
shortcut: "gdp"
timeout: 3
tokens: ["my-token"]

View File

@@ -0,0 +1,16 @@
# This SearXNG setup is used in unit tests
use_default_settings:
engines:
# remove all engines
keep_only: []
engines:
- name: tineye
engine: tineye
categories: ["general"]
shortcut: "tin"
timeout: 9.0
disabled: true

View File

@@ -0,0 +1,141 @@
general:
debug: false
instance_name: "searx"
search:
safe_search: 0
autocomplete: ""
favicon_resolver: ""
default_lang: ""
ban_time_on_fail: 5
max_ban_time_on_fail: 120
server:
port: 9000
bind_address: "[::]"
secret_key: "user_settings_secret"
base_url: false
image_proxy: false
http_protocol_version: "1.0"
method: "POST"
default_http_headers:
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Robots-Tag: noindex, nofollow
Referrer-Policy: no-referrer
ui:
static_path: ""
templates_path: ""
default_theme: simple
default_locale: ""
theme_args:
simple_style: auto
plugins:
searx.plugins.calculator.SXNGPlugin:
active: true
searx.plugins.hash_plugin.SXNGPlugin:
active: true
searx.plugins.self_info.SXNGPlugin:
active: true
searx.plugins.tracker_url_remover.SXNGPlugin:
active: true
searx.plugins.unit_converter.SXNGPlugin:
active: true
searx.plugins.ahmia_filter.SXNGPlugin:
active: true
searx.plugins.hostnames.SXNGPlugin:
active: true
searx.plugins.oa_doi_rewrite.SXNGPlugin:
active: false
searx.plugins.tor_check.SXNGPlugin:
active: false
engines:
- name: wikidata
engine: wikidata
shortcut: wd
timeout: 3.0
weight: 2
- name: wikibooks
engine: mediawiki
shortcut: wb
categories: general
base_url: "https://{language}.wikibooks.org/"
number_of_results: 5
search_type: text
- name: wikinews
engine: mediawiki
shortcut: wn
categories: news
base_url: "https://{language}.wikinews.org/"
number_of_results: 5
search_type: text
- name: wikiquote
engine: mediawiki
shortcut: wq
categories: general
base_url: "https://{language}.wikiquote.org/"
number_of_results: 5
search_type: text
locales:
en: English
ar: العَرَبِيَّة (Arabic)
bg: Български (Bulgarian)
bo: བོད་སྐད་ (Tibetian)
ca: Català (Catalan)
cs: Čeština (Czech)
cy: Cymraeg (Welsh)
da: Dansk (Danish)
de: Deutsch (German)
el_GR: Ελληνικά (Greek_Greece)
eo: Esperanto (Esperanto)
es: Español (Spanish)
et: Eesti (Estonian)
eu: Euskara (Basque)
fa_IR: (fārsī) فارسى (Persian)
fi: Suomi (Finnish)
fil: Wikang Filipino (Filipino)
fr: Français (French)
gl: Galego (Galician)
he: עברית (Hebrew)
hr: Hrvatski (Croatian)
hu: Magyar (Hungarian)
ia: Interlingua (Interlingua)
it: Italiano (Italian)
ja: 日本語 (Japanese)
lt: Lietuvių (Lithuanian)
nl: Nederlands (Dutch)
nl_BE: Vlaams (Dutch_Belgium)
oc: Lenga D'òc (Occitan)
pl: Polski (Polish)
pt: Português (Portuguese)
pt_BR: Português (Portuguese_Brazil)
ro: Română (Romanian)
ru: Русский (Russian)
sk: Slovenčina (Slovak)
sl: Slovenski (Slovene)
sr: српски (Serbian)
sv: Svenska (Swedish)
te: తెలుగు (telugu)
ta: தமிழ் (Tamil)
tr: Türkçe (Turkish)
uk: українська мова (Ukrainian)
vi: tiếng việt (Vietnamese)
zh: 中文 (Chinese)
zh_TW: 國語 (Taiwanese Mandarin)

View File

@@ -0,0 +1,14 @@
use_default_settings:
engines:
keep_only:
- wikibooks
- wikinews
server:
secret_key: "user_secret_key"
bind_address: "[::]"
default_http_headers:
Custom-Header: Custom-Value
engines:
- name: wikipedia
- name: newengine
engine: dummy

View File

@@ -0,0 +1,10 @@
use_default_settings:
engines:
remove:
- wikibooks
- wikinews
server:
secret_key: "user_secret_key"
bind_address: "[::]"
default_http_headers:
Custom-Header: Custom-Value

View File

@@ -0,0 +1,15 @@
use_default_settings:
engines:
remove:
- wikibooks
- wikinews
server:
secret_key: "user_secret_key"
bind_address: "[::]"
default_http_headers:
Custom-Header: Custom-Value
engines:
- name: wikipedia
tokens: ['secret_token']
- name: newengine
engine: dummy

View File

@@ -0,0 +1,6 @@
use_default_settings: true
server:
secret_key: "user_secret_key"
bind_address: "[::]"
default_http_headers:
Custom-Header: Custom-Value

View File

@@ -0,0 +1,34 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from parameterized import parameterized
import searx.plugins
import searx.answerers
import searx.preferences
from searx.extended_types import sxng_request
from tests import SearxTestCase
class AnswererTest(SearxTestCase):
def setUp(self):
super().setUp()
self.storage = searx.plugins.PluginStorage()
engines = {}
self.pref = searx.preferences.Preferences(["simple"], ["general"], engines, self.storage)
self.pref.parse_dict({"locale": "en"})
@parameterized.expand(searx.answerers.STORAGE.answerer_list)
def test_unicode_input(self, answerer_obj: searx.answerers.Answerer):
with self.app.test_request_context():
sxng_request.preferences = self.pref
unicode_payload = "árvíztűrő tükörfúrógép"
for keyword in answerer_obj.keywords:
query = f"{keyword} {unicode_payload}"
self.assertIsInstance(answerer_obj.answer(query), list)

View File

@@ -0,0 +1,101 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring
import logging
from datetime import datetime
from unittest.mock import Mock
from requests import HTTPError
from parameterized import parameterized
import searx.search
import searx.engines
from tests import SearxTestCase
class TinEyeTests(SearxTestCase):
TEST_SETTINGS = "test_tineye.yml"
def setUp(self):
super().setUp()
self.tineye = searx.engines.engines['tineye']
self.tineye.logger.setLevel(logging.INFO)
def tearDown(self):
searx.search.load_engines([])
def test_status_code_raises(self):
response = Mock()
response.status_code = 401
response.raise_for_status.side_effect = HTTPError()
self.assertRaises(HTTPError, lambda: self.tineye.response(response))
@parameterized.expand([(400), (422)])
def test_returns_empty_list(self, status_code):
response = Mock()
response.json.return_value = {"suggestions": {"key": "Download Error"}}
response.status_code = status_code
response.raise_for_status.side_effect = HTTPError()
with self.assertLogs(self.tineye.logger):
results = self.tineye.response(response)
self.assertEqual(0, len(results))
def test_logs_format_for_422(self):
response = Mock()
response.json.return_value = {"suggestions": {"key": "Invalid image URL"}}
response.status_code = 422
response.raise_for_status.side_effect = HTTPError()
with self.assertLogs(self.tineye.logger) as assert_logs_context:
self.tineye.response(response)
self.assertIn(self.tineye.FORMAT_NOT_SUPPORTED, ','.join(assert_logs_context.output))
def test_logs_signature_for_422(self):
response = Mock()
response.json.return_value = {"suggestions": {"key": "NO_SIGNATURE_ERROR"}}
response.status_code = 422
response.raise_for_status.side_effect = HTTPError()
with self.assertLogs(self.tineye.logger) as assert_logs_context:
self.tineye.response(response)
self.assertIn(self.tineye.NO_SIGNATURE_ERROR, ','.join(assert_logs_context.output))
def test_logs_download_for_422(self):
response = Mock()
response.json.return_value = {"suggestions": {"key": "Download Error"}}
response.status_code = 422
response.raise_for_status.side_effect = HTTPError()
with self.assertLogs(self.tineye.logger) as assert_logs_context:
self.tineye.response(response)
self.assertIn(self.tineye.DOWNLOAD_ERROR, ','.join(assert_logs_context.output))
def test_logs_description_for_400(self):
description = 'There was a problem with that request. Error ID: ad5fc955-a934-43c1-8187-f9a61d301645'
response = Mock()
response.json.return_value = {"suggestions": {"description": [description], "title": "Oops! We're sorry!"}}
response.status_code = 400
response.raise_for_status.side_effect = HTTPError()
with self.assertLogs(self.tineye.logger) as assert_logs_context:
self.tineye.response(response)
self.assertIn(description, ','.join(assert_logs_context.output))
def test_crawl_date_parses(self):
date_str = '2020-05-25'
date = datetime.strptime(date_str, '%Y-%m-%d')
response = Mock()
response.json.return_value = {
'matches': [
{
'backlinks': [
{
'crawl_date': date_str,
}
]
}
]
}
response.status_code = 200
results = self.tineye.response(response)
self.assertEqual(date, results[0]['publishedDate'])

View File

@@ -0,0 +1,76 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from searx import settings, engines
from tests import SearxTestCase
class TestEnginesInit(SearxTestCase):
def test_initialize_engines_default(self):
engine_list = [
{'engine': 'dummy', 'name': 'engine1', 'shortcut': 'e1'},
{'engine': 'dummy', 'name': 'engine2', 'shortcut': 'e2'},
]
engines.load_engines(engine_list)
self.assertEqual(len(engines.engines), 2)
self.assertIn('engine1', engines.engines)
self.assertIn('engine2', engines.engines)
def test_initialize_engines_exclude_onions(self):
settings['outgoing']['using_tor_proxy'] = False
engine_list = [
{'engine': 'dummy', 'name': 'engine1', 'shortcut': 'e1', 'categories': 'general'},
{'engine': 'dummy', 'name': 'engine2', 'shortcut': 'e2', 'categories': 'onions'},
]
engines.load_engines(engine_list)
self.assertEqual(len(engines.engines), 1)
self.assertIn('engine1', engines.engines)
self.assertNotIn('onions', engines.categories)
def test_initialize_engines_include_onions(self):
settings['outgoing']['using_tor_proxy'] = True
settings['outgoing']['extra_proxy_timeout'] = 100.0
engine_list = [
{
'engine': 'dummy',
'name': 'engine1',
'shortcut': 'e1',
'categories': 'general',
'timeout': 20.0,
'onion_url': 'http://engine1.onion',
},
{'engine': 'dummy', 'name': 'engine2', 'shortcut': 'e2', 'categories': 'onions'},
]
engines.load_engines(engine_list)
self.assertEqual(len(engines.engines), 2)
self.assertIn('engine1', engines.engines)
self.assertIn('engine2', engines.engines)
self.assertIn('onions', engines.categories)
self.assertIn('http://engine1.onion', engines.engines['engine1'].search_url)
self.assertEqual(engines.engines['engine1'].timeout, 120.0)
def test_missing_name_field(self):
settings['outgoing']['using_tor_proxy'] = False
engine_list = [
{'engine': 'dummy', 'shortcut': 'e1', 'categories': 'general'},
]
with self.assertLogs('searx.engines', level='ERROR') as cm: # pylint: disable=invalid-name
engines.load_engines(engine_list)
self.assertEqual(len(engines.engines), 0)
self.assertEqual(cm.output, ['ERROR:searx.engines:An engine does not have a "name" field'])
def test_missing_engine_field(self):
settings['outgoing']['using_tor_proxy'] = False
engine_list = [
{'name': 'engine2', 'shortcut': 'e2', 'categories': 'onions'},
]
with self.assertLogs('searx.engines', level='ERROR') as cm: # pylint: disable=invalid-name
engines.load_engines(engine_list)
self.assertEqual(len(engines.engines), 0)
self.assertEqual(
cm.output, ['ERROR:searx.engines:The "engine" field is missing for the engine named "engine2"']
)

View File

@@ -0,0 +1,37 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from parameterized import parameterized
from tests import SearxTestCase
import searx.exceptions
from searx import get_setting
class TestExceptions(SearxTestCase):
@parameterized.expand(
[
searx.exceptions.SearxEngineAccessDeniedException,
searx.exceptions.SearxEngineCaptchaException,
searx.exceptions.SearxEngineTooManyRequestsException,
]
)
def test_default_suspend_time(self, exception):
with self.assertRaises(exception) as e:
raise exception()
self.assertEqual(
e.exception.suspended_time,
get_setting(exception.SUSPEND_TIME_SETTING),
)
@parameterized.expand(
[
searx.exceptions.SearxEngineAccessDeniedException,
searx.exceptions.SearxEngineCaptchaException,
searx.exceptions.SearxEngineTooManyRequestsException,
]
)
def test_custom_suspend_time(self, exception):
with self.assertRaises(exception) as e:
raise exception(suspended_time=1337)
self.assertEqual(e.exception.suspended_time, 1337)

View File

@@ -0,0 +1,126 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from searx.external_bang import (
get_node,
resolve_bang_definition,
get_bang_url,
get_bang_definition_and_autocomplete,
LEAF_KEY,
)
from searx.search import SearchQuery, EngineRef
from tests import SearxTestCase
TEST_DB = {
'trie': {
'exam': {
'ple': '//example.com/' + chr(2) + chr(1) + '0',
LEAF_KEY: '//wikipedia.org/wiki/' + chr(2) + chr(1) + '0',
},
'sea': {
LEAF_KEY: 'sea' + chr(2) + chr(1) + '0',
'rch': {
LEAF_KEY: 'search' + chr(2) + chr(1) + '0',
'ing': 'searching' + chr(2) + chr(1) + '0',
},
's': {
'on': 'season' + chr(2) + chr(1) + '0',
'capes': 'seascape' + chr(2) + chr(1) + '0',
},
},
'error': ['error in external_bangs.json'],
}
}
class TestGetNode(SearxTestCase):
DB = { # pylint:disable=invalid-name
'trie': {
'exam': {
'ple': 'test',
LEAF_KEY: 'not used',
}
}
}
def test_found(self):
node, before, after = get_node(TestGetNode.DB, 'example')
self.assertEqual(node, 'test')
self.assertEqual(before, 'example')
self.assertEqual(after, '')
def test_get_partial(self):
node, before, after = get_node(TestGetNode.DB, 'examp')
self.assertEqual(node, TestGetNode.DB['trie']['exam'])
self.assertEqual(before, 'exam')
self.assertEqual(after, 'p')
def test_not_found(self):
node, before, after = get_node(TestGetNode.DB, 'examples')
self.assertEqual(node, 'test')
self.assertEqual(before, 'example')
self.assertEqual(after, 's')
class TestResolveBangDefinition(SearxTestCase):
def test_https(self):
url, rank = resolve_bang_definition('//example.com/' + chr(2) + chr(1) + '42', 'query')
self.assertEqual(url, 'https://example.com/query')
self.assertEqual(rank, 42)
def test_http(self):
url, rank = resolve_bang_definition('http://example.com/' + chr(2) + chr(1) + '0', 'text')
self.assertEqual(url, 'http://example.com/text')
self.assertEqual(rank, 0)
class TestGetBangDefinitionAndAutocomplete(SearxTestCase):
def test_found(self):
bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('exam', external_bangs_db=TEST_DB)
self.assertEqual(bang_definition, TEST_DB['trie']['exam'][LEAF_KEY])
self.assertEqual(new_autocomplete, ['example'])
def test_found_optimized(self):
bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('example', external_bangs_db=TEST_DB)
self.assertEqual(bang_definition, TEST_DB['trie']['exam']['ple'])
self.assertEqual(new_autocomplete, [])
def test_partial(self):
bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('examp', external_bangs_db=TEST_DB)
self.assertIsNone(bang_definition)
self.assertEqual(new_autocomplete, ['example'])
def test_partial2(self):
bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('sea', external_bangs_db=TEST_DB)
self.assertEqual(bang_definition, TEST_DB['trie']['sea'][LEAF_KEY])
self.assertEqual(new_autocomplete, ['search', 'searching', 'seascapes', 'season'])
def test_error(self):
bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('error', external_bangs_db=TEST_DB)
self.assertIsNone(bang_definition)
self.assertEqual(new_autocomplete, [])
def test_actual_data(self):
bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('duckduckgo')
self.assertTrue(bang_definition.startswith('//duckduckgo.com/?q='))
self.assertEqual(new_autocomplete, [])
class TestExternalBangJson(SearxTestCase):
def test_no_external_bang_query(self):
result = get_bang_url(SearchQuery('test', engineref_list=[EngineRef('wikipedia', 'general')]))
self.assertIsNone(result)
def test_get_bang_url(self):
url = get_bang_url(SearchQuery('test', engineref_list=[], external_bang='example'), external_bangs_db=TEST_DB)
self.assertEqual(url, 'https://example.com/test')
def test_actual_data(self):
google_url = get_bang_url(SearchQuery('test', engineref_list=[], external_bang='g'))
self.assertEqual(google_url, 'https://www.google.com/search?q=test')

117
tests/unit/test_locales.py Normal file
View File

@@ -0,0 +1,117 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
"""Test some code from module :py:obj:`searx.locales`"""
from __future__ import annotations
from parameterized import parameterized
from searx import locales
from searx.sxng_locales import sxng_locales
from tests import SearxTestCase
class TestLocales(SearxTestCase):
"""Implemented tests:
- :py:obj:`searx.locales.match_locale`
"""
@classmethod
def setUpClass(cls):
cls.locale_tag_list = [x[0] for x in sxng_locales]
@parameterized.expand(
[
'de',
'fr',
'zh',
]
)
def test_locale_languages(self, locale: str):
# Test SearXNG search languages
self.assertEqual(locales.match_locale(locale, self.locale_tag_list), locale)
@parameterized.expand(
[
('de-at', 'de-AT'),
('de-de', 'de-DE'),
('en-UK', 'en-GB'),
('fr-be', 'fr-BE'),
('fr-ca', 'fr-CA'),
('fr-ch', 'fr-CH'),
('zh-cn', 'zh-CN'),
('zh-tw', 'zh-TW'),
('zh-hk', 'zh-HK'),
]
)
def test_match_region(self, locale: str, expected_locale: str):
# Test SearXNG search regions
self.assertEqual(locales.match_locale(locale, self.locale_tag_list), expected_locale)
@parameterized.expand(
[
('zh-hans', 'zh-CN'),
('zh-hans-cn', 'zh-CN'),
('zh-hant', 'zh-TW'),
('zh-hant-tw', 'zh-TW'),
]
)
def test_match_lang_script_code(self, locale: str, expected_locale: str):
# Test language script code
self.assertEqual(locales.match_locale(locale, self.locale_tag_list), expected_locale)
def test_locale_de(self):
self.assertEqual(locales.match_locale('de', ['de-CH', 'de-DE']), 'de-DE')
self.assertEqual(locales.match_locale('de', ['de-CH', 'de-DE']), 'de-DE')
def test_locale_es(self):
self.assertEqual(locales.match_locale('es', [], fallback='fallback'), 'fallback')
self.assertEqual(locales.match_locale('es', ['ES']), 'ES')
self.assertEqual(locales.match_locale('es', ['es-AR', 'es-ES', 'es-MX']), 'es-ES')
self.assertEqual(locales.match_locale('es-AR', ['es-AR', 'es-ES', 'es-MX']), 'es-AR')
self.assertEqual(locales.match_locale('es-CO', ['es-AR', 'es-ES']), 'es-ES')
self.assertEqual(locales.match_locale('es-CO', ['es-AR']), 'es-AR')
@parameterized.expand(
[
('zh-TW', ['zh-HK'], 'zh-HK'), # A user selects region 'zh-TW' which should end in zh_HK.
# hint: CN is 'Hans' and HK ('Hant') fits better to TW ('Hant')
('zh', ['zh-CN'], 'zh-CN'), # A user selects only the language 'zh' which should end in CN
('fr', ['fr-CA'], 'fr-CA'), # A user selects only the language 'fr' which should end in fr_CA
('nl', ['nl-BE'], 'nl-BE'), # A user selects only the language 'fr' which should end in fr_CA
# Territory tests
('en', ['en-GB'], 'en-GB'), # A user selects only a language
(
'fr',
['fr-FR', 'fr-CA'],
'fr-FR',
), # the engine supports fr_FR and fr_CA since no territory is given, fr_FR takes priority
]
)
def test_locale_optimized_selected(self, locale: str, locale_list: list[str], expected_locale: str):
"""
Tests from the commit message of 9ae409a05a
Assumption:
A. When a user selects a language the results should be optimized according to
the selected language.
"""
self.assertEqual(locales.match_locale(locale, locale_list), expected_locale)
@parameterized.expand(
[
('fr-BE', ['fr-FR', 'fr-CA', 'nl-BE'], 'nl-BE'), # A user selects region 'fr-BE' which should end in nl-BE
('fr', ['fr-BE', 'fr-CH'], 'fr-BE'), # A user selects fr with 2 locales,
# the get_engine_locale selects the locale by looking at the "population
# percent" and this percentage has an higher amount in BE (68.%)
# compared to CH (21%)
]
)
def test_locale_optimized_territory(self, locale: str, locale_list: list[str], expected_locale: str):
"""
Tests from the commit message of 9ae409a05a
B. When user selects a language and a territory the results should be
optimized with first priority on territory and second on language.
"""
self.assertEqual(locales.match_locale(locale, locale_list), expected_locale)

View File

@@ -0,0 +1,93 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from parameterized.parameterized import parameterized
import searx.plugins
import searx.preferences
from searx.extended_types import sxng_request
from searx.result_types import Answer
from tests import SearxTestCase
from .test_utils import random_string
from .test_plugins import do_post_search
class PluginCalculator(SearxTestCase):
def setUp(self):
super().setUp()
engines = {}
self.storage = searx.plugins.PluginStorage()
self.storage.load_settings({"searx.plugins.calculator.SXNGPlugin": {"active": True}})
self.storage.init(self.app)
self.pref = searx.preferences.Preferences(["simple"], ["general"], engines, self.storage)
self.pref.parse_dict({"locale": "en"})
def test_plugin_store_init(self):
self.assertEqual(1, len(self.storage))
def test_pageno_1_2(self):
with self.app.test_request_context():
sxng_request.preferences = self.pref
query = "1+1"
answer = Answer(answer=f"{query} = {eval(query)}") # pylint: disable=eval-used
search = do_post_search(query, self.storage, pageno=1)
self.assertIn(answer, search.result_container.answers)
search = do_post_search(query, self.storage, pageno=2)
self.assertEqual(list(search.result_container.answers), [])
def test_long_query_ignored(self):
with self.app.test_request_context():
sxng_request.preferences = self.pref
query = f"1+1 {random_string(101)}"
search = do_post_search(query, self.storage)
self.assertEqual(list(search.result_container.answers), [])
@parameterized.expand(
[
("1+1", "2", "en"),
("1-1", "0", "en"),
("1*1", "1", "en"),
("1/1", "1", "en"),
("1**1", "1", "en"),
("1^1", "1", "en"),
("1,000.0+1,000.0", "2,000", "en"),
("1.0+1.0", "2", "en"),
("1.0-1.0", "0", "en"),
("1.0*1.0", "1", "en"),
("1.0/1.0", "1", "en"),
("1.0**1.0", "1", "en"),
("1.0^1.0", "1", "en"),
("1.000,0+1.000,0", "2.000", "de"),
("1,0+1,0", "2", "de"),
("1,0-1,0", "0", "de"),
("1,0*1,0", "1", "de"),
("1,0/1,0", "1", "de"),
("1,0**1,0", "1", "de"),
("1,0^1,0", "1", "de"),
]
)
def test_localized_query(self, query: str, res: str, lang: str):
with self.app.test_request_context():
self.pref.parse_dict({"locale": lang})
sxng_request.preferences = self.pref
answer = Answer(answer=f"{query} = {res}")
search = do_post_search(query, self.storage)
self.assertIn(answer, search.result_container.answers)
@parameterized.expand(
[
"1/0",
]
)
def test_invalid_operations(self, query):
with self.app.test_request_context():
sxng_request.preferences = self.pref
search = do_post_search(query, self.storage)
self.assertEqual(list(search.result_container.answers), [])

View File

@@ -0,0 +1,69 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from parameterized.parameterized import parameterized
import searx.plugins
import searx.preferences
from searx.extended_types import sxng_request
from searx.result_types import Answer
from tests import SearxTestCase
from .test_plugins import do_post_search
query_res = [
("md5 test", "md5 hash digest: 098f6bcd4621d373cade4e832627b4f6"),
("sha1 test", "sha1 hash digest: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"),
("sha224 test", "sha224 hash digest: 90a3ed9e32b2aaf4c61c410eb925426119e1a9dc53d4286ade99a809"),
("sha256 test", "sha256 hash digest: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"),
(
"sha384 test",
"sha384 hash digest: 768412320f7b0aa5812fce428dc4706b3c"
"ae50e02a64caa16a782249bfe8efc4b7ef1ccb126255d196047dfedf1"
"7a0a9",
),
(
"sha512 test",
"sha512 hash digest: ee26b0dd4af7e749aa1a8ee3c10ae9923f6"
"18980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5"
"fa9ad8e6f57f50028a8ff",
),
]
class PluginHashTest(SearxTestCase):
def setUp(self):
super().setUp()
engines = {}
self.storage = searx.plugins.PluginStorage()
self.storage.load_settings({"searx.plugins.hash_plugin.SXNGPlugin": {"active": True}})
self.storage.init(self.app)
self.pref = searx.preferences.Preferences(["simple"], ["general"], engines, self.storage)
self.pref.parse_dict({"locale": "en"})
def test_plugin_store_init(self):
self.assertEqual(1, len(self.storage))
@parameterized.expand(query_res)
def test_hash_digest_new(self, query: str, res: str):
with self.app.test_request_context():
sxng_request.preferences = self.pref
answer = Answer(answer=res)
search = do_post_search(query, self.storage)
self.assertIn(answer, search.result_container.answers)
def test_pageno_1_2(self):
with self.app.test_request_context():
sxng_request.preferences = self.pref
query, res = query_res[0]
answer = Answer(answer=res)
search = do_post_search(query, self.storage, pageno=1)
self.assertIn(answer, search.result_container.answers)
search = do_post_search(query, self.storage, pageno=2)
self.assertEqual(list(search.result_container.answers), [])

View File

@@ -0,0 +1,71 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from parameterized.parameterized import parameterized
from flask_babel import gettext
import searx.plugins
import searx.preferences
import searx.limiter
import searx.botdetection
from searx.extended_types import sxng_request
from searx.result_types import Answer
from tests import SearxTestCase
from .test_plugins import do_post_search
class PluginIPSelfInfo(SearxTestCase):
def setUp(self):
super().setUp()
engines = {}
self.storage = searx.plugins.PluginStorage()
self.storage.load_settings({"searx.plugins.self_info.SXNGPlugin": {"active": True}})
self.storage.init(self.app)
self.pref = searx.preferences.Preferences(["simple"], ["general"], engines, self.storage)
self.pref.parse_dict({"locale": "en"})
cfg = searx.limiter.get_cfg()
searx.botdetection.init(cfg, None)
def test_plugin_store_init(self):
self.assertEqual(1, len(self.storage))
def test_pageno_1_2(self):
with self.app.test_request_context():
sxng_request.preferences = self.pref
sxng_request.remote_addr = "127.0.0.1"
sxng_request.headers = {"X-Forwarded-For": "1.2.3.4, 127.0.0.1", "X-Real-IP": "127.0.0.1"} # type: ignore
answer = Answer(answer=gettext("Your IP is: ") + "127.0.0.1")
search = do_post_search("ip", self.storage, pageno=1)
self.assertIn(answer, search.result_container.answers)
search = do_post_search("ip", self.storage, pageno=2)
self.assertEqual(list(search.result_container.answers), [])
@parameterized.expand(
[
"user-agent",
"USER-AgenT lorem ipsum",
]
)
def test_user_agent_in_answer(self, query: str):
query = "user-agent"
with self.app.test_request_context():
sxng_request.preferences = self.pref
sxng_request.user_agent = "Dummy agent" # type: ignore
answer = Answer(answer=gettext("Your user-agent is: ") + "Dummy agent")
search = do_post_search(query, self.storage, pageno=1)
self.assertIn(answer, search.result_container.answers)
search = do_post_search(query, self.storage, pageno=2)
self.assertEqual(list(search.result_container.answers), [])

107
tests/unit/test_plugins.py Normal file
View File

@@ -0,0 +1,107 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
import babel
from mock import Mock
import searx
import searx.plugins
import searx.preferences
import searx.results
from searx.result_types import Result
from searx.extended_types import sxng_request
from tests import SearxTestCase
plg_store = searx.plugins.PluginStorage()
plg_store.load_settings(searx.get_setting("plugins"))
def get_search_mock(query, **kwargs):
lang = kwargs.get("lang", "en-US")
kwargs["pageno"] = kwargs.get("pageno", 1)
kwargs["locale"] = babel.Locale.parse(lang, sep="-")
user_plugins = kwargs.pop("user_plugins", [x.id for x in plg_store])
return Mock(
search_query=Mock(query=query, **kwargs),
user_plugins=user_plugins,
result_container=searx.results.ResultContainer(),
)
def do_pre_search(query, storage, **kwargs) -> bool:
search = get_search_mock(query, **kwargs)
ret = storage.pre_search(sxng_request, search)
return ret
def do_post_search(query, storage, **kwargs) -> Mock:
search = get_search_mock(query, **kwargs)
storage.post_search(sxng_request, search)
return search
class PluginMock(searx.plugins.Plugin):
def __init__(self, _id: str, name: str, active: bool):
plg_cfg = searx.plugins.PluginCfg(active=active)
self.id = _id
self._name = name
super().__init__(plg_cfg)
# pylint: disable= unused-argument
def pre_search(self, request, search) -> bool:
return True
def post_search(self, request, search) -> None:
return None
def on_result(self, request, search, result) -> bool:
return False
def info(self):
return searx.plugins.PluginInfo(
id=self.id,
name=self._name,
description=f"Dummy plugin: {self.id}",
preference_section="general",
)
class PluginStorage(SearxTestCase):
def setUp(self):
super().setUp()
engines = {}
self.storage = searx.plugins.PluginStorage()
self.storage.register(PluginMock("plg001", "first plugin", True))
self.storage.register(PluginMock("plg002", "second plugin", True))
self.storage.init(self.app)
self.pref = searx.preferences.Preferences(["simple"], ["general"], engines, self.storage)
self.pref.parse_dict({"locale": "en"})
def test_init(self):
self.assertEqual(2, len(self.storage))
def test_hooks(self):
with self.app.test_request_context():
sxng_request.preferences = self.pref
query = ""
ret = do_pre_search(query, self.storage, pageno=1)
self.assertTrue(ret is True)
ret = self.storage.on_result(
sxng_request,
get_search_mock("lorem ipsum", user_plugins=["plg001", "plg002"]),
Result(),
)
self.assertFalse(ret)

View File

@@ -0,0 +1,213 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
import flask
from mock import Mock
from searx import favicons
from searx.locales import locales_initialize
from searx.preferences import (
Setting,
EnumStringSetting,
MapSetting,
SearchLanguageSetting,
MultipleChoiceSetting,
PluginsSetting,
ValidationException,
)
import searx.plugins
from searx.preferences import Preferences
from tests import SearxTestCase
from .test_plugins import PluginMock
locales_initialize()
favicons.init()
class TestSettings(SearxTestCase):
# map settings
def test_map_setting_invalid_default_value(self):
with self.assertRaises(ValidationException):
MapSetting(3, map={'dog': 1, 'bat': 2})
def test_map_setting_invalid_choice(self):
setting = MapSetting(2, map={'dog': 1, 'bat': 2})
with self.assertRaises(ValidationException):
setting.parse('cat')
def test_map_setting_valid_default(self):
setting = MapSetting(3, map={'dog': 1, 'bat': 2, 'cat': 3})
self.assertEqual(setting.get_value(), 3)
def test_map_setting_valid_choice(self):
setting = MapSetting(3, map={'dog': 1, 'bat': 2, 'cat': 3})
self.assertEqual(setting.get_value(), 3)
setting.parse('bat')
self.assertEqual(setting.get_value(), 2)
# enum settings
def test_enum_setting_invalid_default_value(self):
with self.assertRaises(ValidationException):
EnumStringSetting('3', choices=['0', '1', '2'])
def test_enum_setting_invalid_choice(self):
setting = EnumStringSetting('0', choices=['0', '1', '2'])
with self.assertRaises(ValidationException):
setting.parse('3')
def test_enum_setting_valid_default(self):
setting = EnumStringSetting('3', choices=['1', '2', '3'])
self.assertEqual(setting.get_value(), '3')
def test_enum_setting_valid_choice(self):
setting = EnumStringSetting('3', choices=['1', '2', '3'])
self.assertEqual(setting.get_value(), '3')
setting.parse('2')
self.assertEqual(setting.get_value(), '2')
# multiple choice settings
def test_multiple_setting_invalid_default_value(self):
with self.assertRaises(ValidationException):
MultipleChoiceSetting(['3', '4'], choices=['0', '1', '2'])
def test_multiple_setting_invalid_choice(self):
setting = MultipleChoiceSetting(['1', '2'], choices=['0', '1', '2'])
with self.assertRaises(ValidationException):
setting.parse('4, 3')
def test_multiple_setting_valid_default(self):
setting = MultipleChoiceSetting(['3'], choices=['1', '2', '3'])
self.assertEqual(setting.get_value(), ['3'])
def test_multiple_setting_valid_choice(self):
setting = MultipleChoiceSetting(['3'], choices=['1', '2', '3'])
self.assertEqual(setting.get_value(), ['3'])
setting.parse('2')
self.assertEqual(setting.get_value(), ['2'])
# search language settings
def test_lang_setting_valid_choice(self):
setting = SearchLanguageSetting('all', choices=['all', 'de', 'en'])
setting.parse('de')
self.assertEqual(setting.get_value(), 'de')
def test_lang_setting_invalid_choice(self):
setting = SearchLanguageSetting('all', choices=['all', 'de', 'en'])
setting.parse('xx')
self.assertEqual(setting.get_value(), 'all')
def test_lang_setting_old_cookie_choice(self):
setting = SearchLanguageSetting('all', choices=['all', 'es', 'es-ES'])
setting.parse('es_XA')
self.assertEqual(setting.get_value(), 'es')
def test_lang_setting_old_cookie_format(self):
setting = SearchLanguageSetting('all', choices=['all', 'es', 'es-ES'])
setting.parse('es_ES')
self.assertEqual(setting.get_value(), 'es-ES')
# plugins settings
def test_plugins_setting_all_default_enabled(self):
storage = searx.plugins.PluginStorage()
storage.register(PluginMock("plg001", "first plugin", True))
storage.register(PluginMock("plg002", "second plugin", True))
plgs_settings = PluginsSetting(False, storage)
self.assertEqual(set(plgs_settings.get_enabled()), {"plg001", "plg002"})
def test_plugins_setting_few_default_enabled(self):
storage = searx.plugins.PluginStorage()
storage.register(PluginMock("plg001", "first plugin", True))
storage.register(PluginMock("plg002", "second plugin", False))
storage.register(PluginMock("plg003", "third plugin", True))
plgs_settings = PluginsSetting(False, storage)
self.assertEqual(set(plgs_settings.get_enabled()), set(['plg001', 'plg003']))
class TestPreferences(SearxTestCase):
def setUp(self):
super().setUp()
storage = searx.plugins.PluginStorage()
self.preferences = Preferences(['simple'], ['general'], {}, storage)
def test_encode(self):
url_params = (
'eJx1Vk1z4zYM_TXxRZNMd7eddg8-pe21nWnvGoiEJEQkofDDtvzrC1qSRdnbQxQTBA'
'Hw8eGRCiJ27AnDsUOHHszBgOsSdHjU-Pr7HwfDCkweHCBFVmxHgxGPB7LiU4-eL9Px'
'TzABDxZjz_r491___HsI0GJA8Ko__nSIPVo8BspLDx5DMjHU7GqH5zpCsyzXTLVMsj'
'mhPzLI8I19d5iX1SFOUkUu4QD6BE6hrpcE8_LPhH6qydWRonjORnItOYqyXHk2Zs1E'
'ARojAdB15GTrMA6VJe_Z13VLBsPL1_ccmk5YUajrBRqxNhSbpAaMdU1Rxkqp13iq6x'
'Np5LxMI15RwtgUSOWx7iqNtyqI3S4Wej6TrmsWfHx2lcD5r-PSa7NWN8glxPxf5r5c'
'ikGrPedw6wZaj1gFbuMZPFaaPKrIAtFceOvJDQSqCNBRJ7BAiGX6TtCEZt0ta2zQd8'
'uwY-4MVqOBqYJxDFvucsbyiXLVd4i6kbUuMeqh8ZA_S1yyutlgIQfFYnLykziFH9vW'
'kB8Uet5iDKQGCEWBhiSln6q80UDlBDch4psPSy1wNZMnVYR2o13m3ASwreQRnceRi2'
'AjSNqOwsqWmbAZxSp_7kcBFnJBeHez4CKpKqieDQgsQREK5fNcBB_H3HrFIUUeJo4s'
'Wx7Abekn6HnHpTM10348UMM8hEejdKbY8ncxfCaO-OgVOHn1ZJX2DRSf8px4eqj6y7'
'dvv162anXS6LYjC3h1YEt_yx-IQ2lxcMo82gw-NVOHdj28EdHH1GDBFYuaQFIMQsrz'
'GZtiyicrqlAYznyhgd2bHFeYHLvJYlHfy_svL7995bOjofp4ef_55fv36zRANbIJA2'
'FX0C_v34oE3Es9oHtQIOFFZcilS5WdV_J5YUHRoeAvdCrZ0IDTCuy4sTOvHvMe96rl'
'usfxs5rcrLuTv1lmOApYmqip6_bEz4eORSyR2xA8tmWxKnkvP3fM0Hgi4bpstFisWR'
'TWV31adSdvSkPc7SkKbtOOTxgny05ALE6pNdL5vhQ5dFQKhYxjbpJZ0ChuSWcN22nh'
'rGpPwC32HXSL7Qm8xf6Dzu6XfLfk19dFoZ4li1sRD9fJVVnWYOmiDCe97Uw0RGi4am'
'o-JJA7IMMYUO7fIvM6N6ZG4ILlotrPhyjXSbSQqQZj7i2d-2pzGntRIHefJS8viwaK'
'-iW6NN9uyTSuTP88CwtKrG-GPaSz6Qn92fwEtGxVk4QMrAhMdev7m6yMBLMOF86iZN'
'JIe_xEadXAQuzW8HltyDCkJrmYVqVOI_oU7ijL64W03LLC81jcA8kFuQpDX1R90-b9'
'_iZOD2J1t9xfE0BGSJ5PqHA7kUUudYuG7HFjz12C2Mz3zNhD8eQgFa_sdiy3InNWHg'
'pV9OCCkWPUZRivRfA2g3DytC3fnlajSaJs4Zihvrwto7eeQxRVR3noCSDzhbZzYKjn'
'd-DZy7PtaVp2WgvPBpzCXUL_J1OGex48RVmOXzBU8_N3kqekkefRDzxNK2_Klp9mBJ'
'wsUnXyRqq1mScHuYalUY7_AZTCR4s=&q='
)
self.preferences.parse_encoded_data(url_params)
self.assertEqual(
vars(self.preferences.key_value_settings['categories']),
{'value': ['general'], 'locked': False, 'choices': ['general', 'none']},
)
def test_save_key_value_setting(self):
setting_key = 'foo'
setting_value = 'bar'
cookie_callback = {}
def set_cookie_callback(name, value, max_age): # pylint: disable=unused-argument
cookie_callback[name] = value
response_mock = Mock(flask.Response)
response_mock.set_cookie = set_cookie_callback
self.preferences.key_value_settings = {
setting_key: Setting(
setting_value,
locked=False,
),
}
self.preferences.save(response_mock)
self.assertIn(setting_key, cookie_callback)
self.assertEqual(cookie_callback[setting_key], setting_value)
def test_false_key_value_setting(self):
setting_key = 'foo'
cookie_callback = {}
def set_cookie_callback(name, value, max_age): # pylint: disable=unused-argument
cookie_callback[name] = value
response_mock = Mock(flask.Response)
response_mock.set_cookie = set_cookie_callback
self.preferences.key_value_settings = {
setting_key: Setting(
'',
locked=True,
),
}
self.preferences.save(response_mock)
self.assertNotIn(setting_key, cookie_callback)

245
tests/unit/test_query.py Normal file
View File

@@ -0,0 +1,245 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from parameterized.parameterized import parameterized
from searx.query import RawTextQuery
from tests import SearxTestCase
class TestQuery(SearxTestCase):
def test_simple_query(self):
query_text = 'the query'
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), query_text)
self.assertEqual(len(query.query_parts), 0)
self.assertEqual(len(query.user_query_parts), 2)
self.assertEqual(len(query.languages), 0)
self.assertFalse(query.specific)
def test_multiple_spaces_query(self):
query_text = '\tthe query'
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), 'the query')
self.assertEqual(len(query.query_parts), 0)
self.assertEqual(len(query.user_query_parts), 2)
self.assertEqual(len(query.languages), 0)
self.assertFalse(query.specific)
def test_str_method(self):
query_text = '<7 the query'
query = RawTextQuery(query_text, [])
self.assertEqual(str(query), '<7 the query')
def test_repr_method(self):
query_text = '<8 the query'
query = RawTextQuery(query_text, [])
r = repr(query)
self.assertTrue(r.startswith(f"<RawTextQuery query='{query_text}' "))
def test_change_query(self):
query_text = '<8 the query'
query = RawTextQuery(query_text, [])
another_query = query.changeQuery('another text')
self.assertEqual(query, another_query)
self.assertEqual(query.getFullQuery(), '<8 another text')
class TestLanguageParser(SearxTestCase):
def test_language_code(self):
language = 'es-ES'
query_text = 'the query'
full_query = ':' + language + ' ' + query_text
query = RawTextQuery(full_query, [])
self.assertEqual(query.getFullQuery(), full_query)
self.assertEqual(len(query.query_parts), 1)
self.assertEqual(len(query.languages), 1)
self.assertIn(language, query.languages)
self.assertFalse(query.specific)
def test_language_name(self):
language = 'english'
query_text = 'the query'
full_query = ':' + language + ' ' + query_text
query = RawTextQuery(full_query, [])
self.assertEqual(query.getFullQuery(), full_query)
self.assertEqual(len(query.query_parts), 1)
self.assertIn('en', query.languages)
self.assertFalse(query.specific)
def test_unlisted_language_code(self):
language = 'all'
query_text = 'the query'
full_query = ':' + language + ' ' + query_text
query = RawTextQuery(full_query, [])
self.assertEqual(query.getFullQuery(), full_query)
self.assertEqual(len(query.query_parts), 1)
self.assertIn('all', query.languages)
self.assertFalse(query.specific)
def test_auto_language_code(self):
language = 'auto'
query_text = 'una consulta'
full_query = ':' + language + ' ' + query_text
query = RawTextQuery(full_query, [])
self.assertEqual(query.getFullQuery(), full_query)
self.assertEqual(len(query.query_parts), 1)
self.assertIn('auto', query.languages)
self.assertFalse(query.specific)
def test_invalid_language_code(self):
language = 'not_a_language'
query_text = 'the query'
full_query = ':' + language + ' ' + query_text
query = RawTextQuery(full_query, [])
self.assertEqual(query.getFullQuery(), full_query)
self.assertEqual(len(query.query_parts), 0)
self.assertEqual(len(query.languages), 0)
self.assertFalse(query.specific)
def test_empty_colon_in_query(self):
query_text = 'the : query'
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), query_text)
self.assertEqual(len(query.query_parts), 0)
self.assertEqual(len(query.languages), 0)
self.assertFalse(query.specific)
def test_autocomplete_empty(self):
query_text = 'the query :'
query = RawTextQuery(query_text, [])
self.assertEqual(query.autocomplete_list, [":en", ":en_us", ":english", ":united_kingdom"])
@parameterized.expand(
[
(':englis', [":english"]),
(':deutschla', [":deutschland"]),
(':new_zea', [":new_zealand"]),
(':zh-', [':zh-cn', ':zh-hk', ':zh-tw']),
]
)
def test_autocomplete(self, query: str, autocomplete_list: list):
query = RawTextQuery(query, [])
self.assertEqual(query.autocomplete_list, autocomplete_list)
class TestTimeoutParser(SearxTestCase):
@parameterized.expand(
[
('<3 the query', 3),
('<350 the query', 0.35),
('<3500 the query', 3.5),
]
)
def test_timeout_limit(self, query_text: str, timeout_limit: float):
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), query_text)
self.assertEqual(len(query.query_parts), 1)
self.assertEqual(query.timeout_limit, timeout_limit)
self.assertFalse(query.specific)
def test_timeout_invalid(self):
# invalid number: it is not bang but it is part of the query
query_text = '<xxx the query'
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), query_text)
self.assertEqual(len(query.query_parts), 0)
self.assertEqual(query.getQuery(), query_text)
self.assertIsNone(query.timeout_limit)
self.assertFalse(query.specific)
def test_timeout_autocomplete(self):
# invalid number: it is not bang but it is part of the query
query_text = 'the query <'
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), query_text)
self.assertEqual(len(query.query_parts), 0)
self.assertEqual(query.getQuery(), query_text)
self.assertIsNone(query.timeout_limit)
self.assertFalse(query.specific)
self.assertEqual(query.autocomplete_list, ['<3', '<850'])
class TestExternalBangParser(SearxTestCase):
def test_external_bang(self):
query_text = '!!ddg the query'
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), query_text)
self.assertEqual(len(query.query_parts), 1)
self.assertFalse(query.specific)
def test_external_bang_not_found(self):
query_text = '!!notfoundbang the query'
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), query_text)
self.assertIsNone(query.external_bang)
self.assertFalse(query.specific)
def test_external_bang_autocomplete(self):
query_text = 'the query !!dd'
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), '!!dd the query')
self.assertEqual(len(query.query_parts), 1)
self.assertFalse(query.specific)
self.assertGreater(len(query.autocomplete_list), 0)
a = query.autocomplete_list[0]
self.assertEqual(query.get_autocomplete_full_query(a), a + ' the query')
class TestBang(SearxTestCase):
SPECIFIC_BANGS = ['!dummy_engine', '!gd', '!general']
THE_QUERY = 'the query'
@parameterized.expand(SPECIFIC_BANGS)
def test_bang(self, bang: str):
with self.subTest(msg="Check bang", bang=bang):
query_text = TestBang.THE_QUERY + ' ' + bang
query = RawTextQuery(query_text, [])
self.assertEqual(query.getFullQuery(), bang + ' ' + TestBang.THE_QUERY)
self.assertEqual(query.query_parts, [bang])
self.assertEqual(query.user_query_parts, TestBang.THE_QUERY.split(' '))
@parameterized.expand(SPECIFIC_BANGS)
def test_specific(self, bang: str):
with self.subTest(msg="Check bang is specific", bang=bang):
query_text = TestBang.THE_QUERY + ' ' + bang
query = RawTextQuery(query_text, [])
self.assertTrue(query.specific)
def test_bang_not_found(self):
query = RawTextQuery('the query !bang_not_found', [])
self.assertEqual(query.getFullQuery(), 'the query !bang_not_found')
def test_bang_autocomplete(self):
query = RawTextQuery('the query !dum', [])
self.assertEqual(query.autocomplete_list, ['!dummy_engine', '!dummy_private_engine'])
query = RawTextQuery('!dum the query', [])
self.assertEqual(query.autocomplete_list, [])
self.assertEqual(query.getQuery(), '!dum the query')
def test_bang_autocomplete_empty(self):
query = RawTextQuery('the query !', [])
self.assertEqual(query.autocomplete_list, ['!images', '!wikipedia', '!osm'])
query = RawTextQuery('the query !', ['osm'])
self.assertEqual(query.autocomplete_list, ['!images', '!wikipedia'])

View File

@@ -0,0 +1,60 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from searx.result_types import LegacyResult
from searx.results import ResultContainer
from tests import SearxTestCase
class ResultContainerTestCase(SearxTestCase):
# pylint: disable=use-dict-literal
TEST_SETTINGS = "test_result_container.yml"
def test_empty(self):
container = ResultContainer()
self.assertEqual(container.get_ordered_results(), [])
def test_one_result(self):
result = dict(url="https://example.org", title="title ..", content="Lorem ..")
container = ResultContainer()
container.extend("google", [result])
container.close()
self.assertEqual(len(container.get_ordered_results()), 1)
res = LegacyResult(result)
res.normalize_result_fields()
self.assertIn(res, container.get_ordered_results())
def test_one_suggestion(self):
result = dict(suggestion="lorem ipsum ..")
container = ResultContainer()
container.extend("duckduckgo", [result])
container.close()
self.assertEqual(len(container.get_ordered_results()), 0)
self.assertEqual(len(container.suggestions), 1)
self.assertIn(result["suggestion"], container.suggestions)
def test_merge_url_result(self):
# from the merge of eng1 and eng2 we expect this result
result = LegacyResult(
url="https://example.org", title="very long title, lorem ipsum", content="Lorem ipsum dolor sit amet .."
)
result.normalize_result_fields()
eng1 = dict(url=result.url, title="short title", content=result.content, engine="google")
eng2 = dict(url="http://example.org", title=result.title, content="lorem ipsum", engine="duckduckgo")
container = ResultContainer()
container.extend(None, [eng1, eng2])
container.close()
result_list = container.get_ordered_results()
self.assertEqual(len(container.get_ordered_results()), 1)
self.assertIn(result, result_list)
self.assertEqual(result_list[0].title, result.title)
self.assertEqual(result_list[0].content, result.content)

120
tests/unit/test_search.py Normal file
View File

@@ -0,0 +1,120 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from copy import copy
import searx.search
from searx.search import SearchQuery, EngineRef
from searx import settings
from tests import SearxTestCase
SAFESEARCH = 0
PAGENO = 1
PUBLIC_ENGINE_NAME = "dummy engine" # from the ./settings/test_settings.yml
class SearchQueryTestCase(SearxTestCase):
def test_repr(self):
s = SearchQuery('test', [EngineRef('bing', 'general')], 'all', 0, 1, '1', 5.0, 'g')
self.assertEqual(
repr(s), "SearchQuery('test', [EngineRef('bing', 'general')], 'all', 0, 1, '1', 5.0, 'g', None)"
) # noqa
def test_eq(self):
s = SearchQuery('test', [EngineRef('bing', 'general')], 'all', 0, 1, None, None, None)
t = SearchQuery('test', [EngineRef('google', 'general')], 'all', 0, 1, None, None, None)
self.assertEqual(s, s)
self.assertNotEqual(s, t)
def test_copy(self):
s = SearchQuery('test', [EngineRef('bing', 'general')], 'all', 0, 1, None, None, None)
t = copy(s)
self.assertEqual(s, t)
class SearchTestCase(SearxTestCase):
def test_timeout_simple(self):
settings['outgoing']['max_request_timeout'] = None
search_query = SearchQuery(
'test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')], 'en-US', SAFESEARCH, PAGENO, None, None
)
search = searx.search.Search(search_query)
with self.app.test_request_context('/search'):
search.search()
self.assertEqual(search.actual_timeout, 3.0)
def test_timeout_query_above_default_nomax(self):
settings['outgoing']['max_request_timeout'] = None
search_query = SearchQuery(
'test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')], 'en-US', SAFESEARCH, PAGENO, None, 5.0
)
search = searx.search.Search(search_query)
with self.app.test_request_context('/search'):
search.search()
self.assertEqual(search.actual_timeout, 3.0)
def test_timeout_query_below_default_nomax(self):
settings['outgoing']['max_request_timeout'] = None
search_query = SearchQuery(
'test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')], 'en-US', SAFESEARCH, PAGENO, None, 1.0
)
search = searx.search.Search(search_query)
with self.app.test_request_context('/search'):
search.search()
self.assertEqual(search.actual_timeout, 1.0)
def test_timeout_query_below_max(self):
settings['outgoing']['max_request_timeout'] = 10.0
search_query = SearchQuery(
'test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')], 'en-US', SAFESEARCH, PAGENO, None, 5.0
)
search = searx.search.Search(search_query)
with self.app.test_request_context('/search'):
search.search()
self.assertEqual(search.actual_timeout, 5.0)
def test_timeout_query_above_max(self):
settings['outgoing']['max_request_timeout'] = 10.0
search_query = SearchQuery(
'test', [EngineRef(PUBLIC_ENGINE_NAME, 'general')], 'en-US', SAFESEARCH, PAGENO, None, 15.0
)
search = searx.search.Search(search_query)
with self.app.test_request_context('/search'):
search.search()
self.assertEqual(search.actual_timeout, 10.0)
def test_external_bang_valid(self):
search_query = SearchQuery(
'yes yes',
[EngineRef(PUBLIC_ENGINE_NAME, 'general')],
'en-US',
SAFESEARCH,
PAGENO,
None,
None,
external_bang="yt",
)
search = searx.search.Search(search_query)
results = search.search()
# For checking if the user redirected with the youtube external bang
self.assertIsNotNone(results.redirect_url)
def test_external_bang_none(self):
search_query = SearchQuery(
'youtube never gonna give you up',
[EngineRef(PUBLIC_ENGINE_NAME, 'general')],
'en-US',
SAFESEARCH,
PAGENO,
None,
None,
)
search = searx.search.Search(search_query)
with self.app.test_request_context('/search'):
results = search.search()
# This should not redirect
self.assertIsNone(results.redirect_url)

View File

@@ -0,0 +1,119 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from pathlib import Path
import os
from unittest.mock import patch
from parameterized import parameterized
from searx.exceptions import SearxSettingsException
from searx import settings_loader
from tests import SearxTestCase
def _settings(f_name):
return str(Path(__file__).parent.absolute() / "settings" / f_name)
class TestLoad(SearxTestCase):
def test_load_zero(self):
with self.assertRaises(SearxSettingsException):
settings_loader.load_yaml('/dev/zero')
with self.assertRaises(SearxSettingsException):
settings_loader.load_yaml(_settings("syntaxerror_settings.yml"))
self.assertEqual(settings_loader.load_yaml(_settings("empty_settings.yml")), {})
class TestDefaultSettings(SearxTestCase):
def test_load(self):
settings, msg = settings_loader.load_settings(load_user_settings=False)
self.assertTrue(msg.startswith('load the default settings from'))
self.assertFalse(settings['general']['debug'])
self.assertIsInstance(settings['general']['instance_name'], str)
self.assertEqual(settings['server']['secret_key'], "ultrasecretkey")
self.assertIsInstance(settings['server']['port'], int)
self.assertIsInstance(settings['server']['bind_address'], str)
self.assertIsInstance(settings['engines'], list)
self.assertIsInstance(settings['doi_resolvers'], dict)
self.assertIsInstance(settings['default_doi_resolver'], str)
class TestUserSettings(SearxTestCase):
def test_is_use_default_settings(self):
self.assertFalse(settings_loader.is_use_default_settings({}))
self.assertTrue(settings_loader.is_use_default_settings({'use_default_settings': True}))
self.assertTrue(settings_loader.is_use_default_settings({'use_default_settings': {}}))
with self.assertRaises(ValueError):
self.assertFalse(settings_loader.is_use_default_settings({'use_default_settings': 1}))
with self.assertRaises(ValueError):
self.assertFalse(settings_loader.is_use_default_settings({'use_default_settings': 0}))
@parameterized.expand(
[
_settings("not_exists.yml"),
"/folder/not/exists",
]
)
def test_user_settings_not_found(self, path: str):
with patch.dict(os.environ, {'SEARXNG_SETTINGS_PATH': path}):
with self.assertRaises(EnvironmentError):
_s, _m = settings_loader.load_settings()
def test_user_settings(self):
with patch.dict(os.environ, {'SEARXNG_SETTINGS_PATH': _settings("user_settings_simple.yml")}):
settings, msg = settings_loader.load_settings()
self.assertTrue(msg.startswith('merge the default settings'))
self.assertEqual(settings['server']['secret_key'], "user_secret_key")
self.assertEqual(settings['server']['default_http_headers']['Custom-Header'], "Custom-Value")
def test_user_settings_remove(self):
with patch.dict(os.environ, {'SEARXNG_SETTINGS_PATH': _settings("user_settings_remove.yml")}):
settings, msg = settings_loader.load_settings()
self.assertTrue(msg.startswith('merge the default settings'))
self.assertEqual(settings['server']['secret_key'], "user_secret_key")
self.assertEqual(settings['server']['default_http_headers']['Custom-Header'], "Custom-Value")
engine_names = [engine['name'] for engine in settings['engines']]
self.assertNotIn('wikinews', engine_names)
self.assertNotIn('wikibooks', engine_names)
self.assertIn('wikipedia', engine_names)
def test_user_settings_remove2(self):
with patch.dict(os.environ, {'SEARXNG_SETTINGS_PATH': _settings("user_settings_remove2.yml")}):
settings, msg = settings_loader.load_settings()
self.assertTrue(msg.startswith('merge the default settings'))
self.assertEqual(settings['server']['secret_key'], "user_secret_key")
self.assertEqual(settings['server']['default_http_headers']['Custom-Header'], "Custom-Value")
engine_names = [engine['name'] for engine in settings['engines']]
self.assertNotIn('wikinews', engine_names)
self.assertNotIn('wikibooks', engine_names)
self.assertIn('wikipedia', engine_names)
wikipedia = list(filter(lambda engine: (engine.get('name')) == 'wikipedia', settings['engines']))
self.assertEqual(wikipedia[0]['engine'], 'wikipedia')
self.assertEqual(wikipedia[0]['tokens'], ['secret_token'])
newengine = list(filter(lambda engine: (engine.get('name')) == 'newengine', settings['engines']))
self.assertEqual(newengine[0]['engine'], 'dummy')
def test_user_settings_keep_only(self):
with patch.dict(os.environ, {'SEARXNG_SETTINGS_PATH': _settings("user_settings_keep_only.yml")}):
settings, msg = settings_loader.load_settings()
self.assertTrue(msg.startswith('merge the default settings'))
engine_names = [engine['name'] for engine in settings['engines']]
self.assertEqual(engine_names, ['wikibooks', 'wikinews', 'wikipedia', 'newengine'])
# wikipedia has been removed, then added again with the "engine" section of user_settings_keep_only.yml
self.assertEqual(len(settings['engines'][2]), 1)
def test_custom_settings(self):
with patch.dict(os.environ, {'SEARXNG_SETTINGS_PATH': _settings("user_settings.yml")}):
settings, msg = settings_loader.load_settings()
self.assertTrue(msg.startswith('load the user settings from'))
self.assertEqual(settings['server']['port'], 9000)
self.assertEqual(settings['server']['secret_key'], "user_settings_secret")
engine_names = [engine['name'] for engine in settings['engines']]
self.assertEqual(engine_names, ['wikidata', 'wikibooks', 'wikinews', 'wikiquote'])

13
tests/unit/test_toml.py Normal file
View File

@@ -0,0 +1,13 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
from tests import SearxTestCase
from searx import compat
from searx.favicons.config import DEFAULT_CFG_TOML_PATH
class CompatTest(SearxTestCase):
def test_toml(self):
with DEFAULT_CFG_TOML_PATH.open("rb") as f:
_ = compat.tomllib.load(f)

246
tests/unit/test_utils.py Normal file
View File

@@ -0,0 +1,246 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
import random
import string
import lxml.etree
from lxml import html
from parameterized.parameterized import parameterized
from searx.exceptions import SearxXPathSyntaxException, SearxEngineXPathException
from searx import utils
from tests import SearxTestCase
def random_string(length, choices=string.ascii_letters):
return ''.join(random.choice(choices) for _ in range(length))
class TestUtils(SearxTestCase):
def test_gen_useragent(self):
self.assertIsInstance(utils.gen_useragent(), str)
self.assertIsNotNone(utils.gen_useragent())
self.assertTrue(utils.gen_useragent().startswith('Mozilla'))
def test_searx_useragent(self):
self.assertIsInstance(utils.searx_useragent(), str)
self.assertIsNotNone(utils.searx_useragent())
self.assertTrue(utils.searx_useragent().startswith('searx'))
def test_html_to_text(self):
html_str = """
<a href="/testlink" class="link_access_account">
<style>
.toto {
color: red;
}
</style>
<span class="toto">
<span>
<img src="test.jpg" />
</span>
</span>
<span class="titi">
Test text
</span>
<script>value='dummy';</script>
</a>
"""
self.assertIsInstance(utils.html_to_text(html_str), str)
self.assertIsNotNone(utils.html_to_text(html_str))
self.assertEqual(utils.html_to_text(html_str), "Test text")
self.assertEqual(utils.html_to_text(r"regexp: (?<![a-zA-Z]"), "regexp: (?<![a-zA-Z]")
def test_extract_text(self):
html_str = """
<a href="/testlink" class="link_access_account">
<span class="toto">
<span>
<img src="test.jpg" />
</span>
</span>
<span class="titi">
Test text
</span>
</a>
"""
dom = html.fromstring(html_str)
self.assertEqual(utils.extract_text(dom), 'Test text')
self.assertEqual(utils.extract_text(dom.xpath('//span')), 'Test text')
self.assertEqual(utils.extract_text(dom.xpath('//span/text()')), 'Test text')
self.assertEqual(utils.extract_text(dom.xpath('count(//span)')), '3.0')
self.assertEqual(utils.extract_text(dom.xpath('boolean(//span)')), 'True')
self.assertEqual(utils.extract_text(dom.xpath('//img/@src')), 'test.jpg')
self.assertEqual(utils.extract_text(dom.xpath('//unexistingtag')), '')
def test_extract_text_allow_none(self):
self.assertEqual(utils.extract_text(None, allow_none=True), None)
def test_extract_text_error_none(self):
with self.assertRaises(ValueError):
utils.extract_text(None)
def test_extract_text_error_empty(self):
with self.assertRaises(ValueError):
utils.extract_text({})
def test_extract_url(self):
def f(html_str, search_url):
return utils.extract_url(html.fromstring(html_str), search_url)
self.assertEqual(f('<span id="42">https://example.com</span>', 'http://example.com/'), 'https://example.com/')
self.assertEqual(f('https://example.com', 'http://example.com/'), 'https://example.com/')
self.assertEqual(f('//example.com', 'http://example.com/'), 'http://example.com/')
self.assertEqual(f('//example.com', 'https://example.com/'), 'https://example.com/')
self.assertEqual(f('/path?a=1', 'https://example.com'), 'https://example.com/path?a=1')
with self.assertRaises(lxml.etree.ParserError):
f('', 'https://example.com')
with self.assertRaises(Exception):
utils.extract_url([], 'https://example.com')
def test_html_to_text_invalid(self):
_html = '<p><b>Lorem ipsum</i>dolor sit amet</p>'
self.assertEqual(utils.html_to_text(_html), "Lorem ipsum")
def test_ecma_unscape(self):
self.assertEqual(utils.ecma_unescape('text%20with%20space'), 'text with space')
self.assertEqual(utils.ecma_unescape('text using %xx: %F3'), 'text using %xx: ó')
self.assertEqual(utils.ecma_unescape('text using %u: %u5409, %u4E16%u754c'), 'text using %u: 吉, 世界')
class TestHTMLTextExtractor(SearxTestCase): # pylint: disable=missing-class-docstring
def setUp(self):
super().setUp()
self.html_text_extractor = utils._HTMLTextExtractor() # pylint: disable=protected-access
def test__init__(self):
self.assertEqual(self.html_text_extractor.result, [])
@parameterized.expand(
[
('xF', '\x0f'),
('XF', '\x0f'),
('97', 'a'),
]
)
def test_handle_charref(self, charref: str, expected: str):
self.html_text_extractor.handle_charref(charref)
self.assertIn(expected, self.html_text_extractor.result)
def test_handle_entityref(self):
entity = 'test'
self.html_text_extractor.handle_entityref(entity)
self.assertIn(entity, self.html_text_extractor.result)
def test_invalid_html(self):
text = '<p><b>Lorem ipsum</i>dolor sit amet</p>'
with self.assertRaises(utils._HTMLTextExtractorException): # pylint: disable=protected-access
self.html_text_extractor.feed(text)
class TestXPathUtils(SearxTestCase): # pylint: disable=missing-class-docstring
TEST_DOC = """<ul>
<li>Text in <b>bold</b> and <i>italic</i> </li>
<li>Another <b>text</b> <img src="data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="></li>
</ul>"""
def test_get_xpath_cache(self):
xp1 = utils.get_xpath('//a')
xp2 = utils.get_xpath('//div')
xp3 = utils.get_xpath('//a')
self.assertEqual(id(xp1), id(xp3))
self.assertNotEqual(id(xp1), id(xp2))
def test_get_xpath_type(self):
utils.get_xpath(lxml.etree.XPath('//a'))
with self.assertRaises(TypeError):
utils.get_xpath([])
def test_get_xpath_invalid(self):
invalid_xpath = '//a[0].text'
with self.assertRaises(SearxXPathSyntaxException) as context:
utils.get_xpath(invalid_xpath)
self.assertEqual(context.exception.message, 'Invalid expression')
self.assertEqual(context.exception.xpath_str, invalid_xpath)
def test_eval_xpath_unregistered_function(self):
doc = html.fromstring(TestXPathUtils.TEST_DOC)
invalid_function_xpath = 'int(//a)'
with self.assertRaises(SearxEngineXPathException) as context:
utils.eval_xpath(doc, invalid_function_xpath)
self.assertEqual(context.exception.message, 'Unregistered function')
self.assertEqual(context.exception.xpath_str, invalid_function_xpath)
def test_eval_xpath(self):
doc = html.fromstring(TestXPathUtils.TEST_DOC)
self.assertEqual(utils.eval_xpath(doc, '//p'), [])
self.assertEqual(utils.eval_xpath(doc, '//i/text()'), ['italic'])
self.assertEqual(utils.eval_xpath(doc, 'count(//i)'), 1.0)
def test_eval_xpath_list(self):
doc = html.fromstring(TestXPathUtils.TEST_DOC)
# check a not empty list
self.assertEqual(utils.eval_xpath_list(doc, '//i/text()'), ['italic'])
# check min_len parameter
with self.assertRaises(SearxEngineXPathException) as context:
utils.eval_xpath_list(doc, '//p', min_len=1)
self.assertEqual(context.exception.message, 'len(xpath_str) < 1')
self.assertEqual(context.exception.xpath_str, '//p')
def test_eval_xpath_getindex(self):
doc = html.fromstring(TestXPathUtils.TEST_DOC)
# check index 0
self.assertEqual(utils.eval_xpath_getindex(doc, '//i/text()', 0), 'italic')
# default is 'something'
self.assertEqual(utils.eval_xpath_getindex(doc, '//i/text()', 1, default='something'), 'something')
# default is None
self.assertIsNone(utils.eval_xpath_getindex(doc, '//i/text()', 1, default=None))
# index not found
with self.assertRaises(SearxEngineXPathException) as context:
utils.eval_xpath_getindex(doc, '//i/text()', 1)
self.assertEqual(context.exception.message, 'index 1 not found')
# not a list
with self.assertRaises(SearxEngineXPathException) as context:
utils.eval_xpath_getindex(doc, 'count(//i)', 1)
self.assertEqual(context.exception.message, 'the result is not a list')
def test_detect_language(self):
# make sure new line are not an issue
# fasttext.predict('') does not accept new line.
l = utils.detect_language('The quick brown fox jumps over\nthe lazy dog')
self.assertEqual(l, 'en')
l = utils.detect_language(
'いろはにほへと ちりぬるを わかよたれそ つねならむ うゐのおくやま けふこえて あさきゆめみし ゑひもせす'
)
self.assertEqual(l, 'ja')
l = utils.detect_language('Pijamalı hasta yağız şoföre çabucak güvendi.')
self.assertEqual(l, 'tr')
l = utils.detect_language('')
self.assertIsNone(l)
# mix languages --> None
l = utils.detect_language('The いろはにほへと Pijamalı')
self.assertIsNone(l)
with self.assertRaises(ValueError):
utils.detect_language(None) # type: ignore

View File

@@ -0,0 +1,40 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
import searx.plugins
from searx.engines import engines
from searx.preferences import Preferences
from searx.search import EngineRef
from searx.webadapter import validate_engineref_list
from tests import SearxTestCase
PRIVATE_ENGINE_NAME = "dummy private engine" # from the ./settings/test_settings.yml
SEARCHQUERY = [EngineRef(PRIVATE_ENGINE_NAME, "general")]
class ValidateQueryCase(SearxTestCase):
def test_without_token(self):
preferences = Preferences(['simple'], ['general'], engines, searx.plugins.STORAGE)
valid, unknown, invalid_token = validate_engineref_list(SEARCHQUERY, preferences)
self.assertEqual(len(valid), 0)
self.assertEqual(len(unknown), 0)
self.assertEqual(len(invalid_token), 1)
def test_with_incorrect_token(self):
preferences_with_tokens = Preferences(['simple'], ['general'], engines, searx.plugins.STORAGE)
preferences_with_tokens.parse_dict({'tokens': 'bad-token'})
valid, unknown, invalid_token = validate_engineref_list(SEARCHQUERY, preferences_with_tokens)
self.assertEqual(len(valid), 0)
self.assertEqual(len(unknown), 0)
self.assertEqual(len(invalid_token), 1)
def test_with_correct_token(self):
preferences_with_tokens = Preferences(['simple'], ['general'], engines, searx.plugins.STORAGE)
preferences_with_tokens.parse_dict({'tokens': 'my-token'})
valid, unknown, invalid_token = validate_engineref_list(SEARCHQUERY, preferences_with_tokens)
self.assertEqual(len(valid), 1)
self.assertEqual(len(unknown), 0)
self.assertEqual(len(invalid_token), 0)

250
tests/unit/test_webapp.py Normal file
View File

@@ -0,0 +1,250 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
import json
import babel
from mock import Mock
import searx.webapp
import searx.search
import searx.search.processors
from searx.result_types._base import MainResult
from searx.results import Timing
from searx.preferences import Preferences
from tests import SearxTestCase
class ViewsTestCase(SearxTestCase): # pylint: disable=too-many-public-methods
def setUp(self):
super().setUp()
# skip init function (no external HTTP request)
def dummy(*args, **kwargs): # pylint: disable=unused-argument
pass
self.setattr4test(searx.search.processors, 'initialize_processor', dummy)
# remove sha for the static file so the tests don't have to care about
# the changing URLs
self.setattr4test(searx.webapp, 'static_files', {})
# set some defaults
test_results = [
MainResult(
title="First Test",
url="http://first.test.xyz",
content="first test content",
engine="startpage",
),
MainResult(
title="Second Test",
url="http://second.test.xyz",
content="second test content",
engine="youtube",
),
]
for r in test_results:
r.normalize_result_fields()
timings = [
Timing(engine='startpage', total=0.8, load=0.7),
Timing(engine='youtube', total=0.9, load=0.6),
]
def search_mock(search_self, *args): # pylint: disable=unused-argument
search_self.result_container = Mock(
get_ordered_results=lambda: test_results,
answers={},
corrections=set(),
suggestions=set(),
infoboxes=[],
unresponsive_engines=set(),
results=test_results,
number_of_results=3,
results_length=lambda: len(test_results),
get_timings=lambda: timings,
redirect_url=None,
engine_data={},
)
search_self.search_query.locale = babel.Locale.parse("en-US", sep='-')
self.setattr4test(searx.search.Search, 'search', search_mock)
original_preferences_get_value = Preferences.get_value
def preferences_get_value(preferences_self, user_setting_name: str):
if user_setting_name == 'theme':
return 'simple'
return original_preferences_get_value(preferences_self, user_setting_name)
self.setattr4test(Preferences, 'get_value', preferences_get_value)
# to see full diffs
self.maxDiff = None # pylint: disable=invalid-name
def test_index_empty(self):
result = self.client.post('/')
self.assertEqual(result.status_code, 200)
self.assertIn(
b'<div class="title"><h1>SearXNG</h1></div>',
result.data,
)
def test_index_html_post(self):
result = self.client.post('/', data={'q': 'test'})
self.assertEqual(result.status_code, 308)
self.assertEqual(result.location, '/search')
def test_index_html_get(self):
result = self.client.post('/?q=test')
self.assertEqual(result.status_code, 308)
self.assertEqual(result.location, '/search?q=test')
def test_search_empty_html(self):
result = self.client.post('/search', data={'q': ''})
self.assertEqual(result.status_code, 200)
self.assertIn(b'<div class="title"><h1>SearXNG</h1></div>', result.data)
def test_search_empty_json(self):
result = self.client.post('/search', data={'q': '', 'format': 'json'})
self.assertEqual(result.status_code, 400)
def test_search_empty_csv(self):
result = self.client.post('/search', data={'q': '', 'format': 'csv'})
self.assertEqual(result.status_code, 400)
def test_search_empty_rss(self):
result = self.client.post('/search', data={'q': '', 'format': 'rss'})
self.assertEqual(result.status_code, 400)
def test_search_html(self):
result = self.client.post('/search', data={'q': 'test'})
self.assertIn(
b'<span class="url_o1"><span class="url_i1">http://second.test.xyz</span></span>',
result.data,
)
self.assertIn(
b'<p class="content">\n second <span class="highlight">test</span> ',
result.data,
)
def test_index_json(self):
result = self.client.post('/', data={'q': 'test', 'format': 'json'})
self.assertEqual(result.status_code, 308)
def test_search_json(self):
result = self.client.post('/search', data={'q': 'test', 'format': 'json'})
result_dict = json.loads(result.data.decode())
self.assertEqual('test', result_dict['query'])
self.assertEqual(len(result_dict['results']), 2)
self.assertEqual(result_dict['results'][0]['content'], 'first test content')
self.assertEqual(result_dict['results'][0]['url'], 'http://first.test.xyz')
def test_index_csv(self):
result = self.client.post('/', data={'q': 'test', 'format': 'csv'})
self.assertEqual(result.status_code, 308)
def test_search_csv(self):
result = self.client.post('/search', data={'q': 'test', 'format': 'csv'})
self.assertEqual(
b'title,url,content,host,engine,score,type\r\n'
+ b'First Test,http://first.test.xyz,first test content,first.test.xyz,startpage,0,result\r\n'
+ b'Second Test,http://second.test.xyz,second test content,second.test.xyz,youtube,0,result\r\n',
result.data,
)
def test_index_rss(self):
result = self.client.post('/', data={'q': 'test', 'format': 'rss'})
self.assertEqual(result.status_code, 308)
def test_search_rss(self):
result = self.client.post('/search', data={'q': 'test', 'format': 'rss'})
self.assertIn(b'<description>Search results for "test" - SearXNG</description>', result.data)
self.assertIn(b'<opensearch:totalResults>3</opensearch:totalResults>', result.data)
self.assertIn(b'<title>First Test</title>', result.data)
self.assertIn(b'<link>http://first.test.xyz</link>', result.data)
self.assertIn(b'<description>first test content</description>', result.data)
def test_redirect_about(self):
result = self.client.get('/about')
self.assertEqual(result.status_code, 302)
def test_info_page(self):
result = self.client.get('/info/en/search-syntax')
self.assertEqual(result.status_code, 200)
self.assertIn(b'<h1>Search syntax</h1>', result.data)
def test_health(self):
result = self.client.get('/healthz')
self.assertEqual(result.status_code, 200)
self.assertIn(b'OK', result.data)
def test_preferences(self):
result = self.client.get('/preferences')
self.assertEqual(result.status_code, 200)
self.assertIn(b'<form id="search_form" method="post" action="/preferences"', result.data)
self.assertIn(b'<div id="categories_container">', result.data)
self.assertIn(b'<legend id="pref_ui_locale">Interface language</legend>', result.data)
def test_browser_locale(self):
result = self.client.get('/preferences', headers={'Accept-Language': 'zh-tw;q=0.8'})
self.assertEqual(result.status_code, 200)
self.assertIn(
b'<option value="zh-Hant-TW" selected="selected">',
result.data,
'Interface locale ignored browser preference.',
)
self.assertIn(
b'<option value="zh-Hant-TW" selected="selected">',
result.data,
'Search language ignored browser preference.',
)
def test_browser_empty_locale(self):
result = self.client.get('/preferences', headers={'Accept-Language': ''})
self.assertEqual(result.status_code, 200)
self.assertIn(
b'<option value="en" selected="selected">', result.data, 'Interface locale ignored browser preference.'
)
def test_locale_occitan(self):
result = self.client.get('/preferences?locale=oc')
self.assertEqual(result.status_code, 200)
self.assertIn(
b'<option value="oc" selected="selected">', result.data, 'Interface locale ignored browser preference.'
)
def test_stats(self):
result = self.client.get('/stats')
self.assertEqual(result.status_code, 200)
self.assertIn(b'<h1>Engine stats</h1>', result.data)
def test_robots_txt(self):
result = self.client.get('/robots.txt')
self.assertEqual(result.status_code, 200)
self.assertIn(b'Allow: /', result.data)
def test_opensearch_xml(self):
result = self.client.get('/opensearch.xml')
self.assertEqual(result.status_code, 200)
self.assertIn(
b'<Description>SearXNG is a metasearch engine that respects your privacy.</Description>', result.data
)
def test_favicon(self):
result = self.client.get('/favicon.ico')
result.close()
self.assertEqual(result.status_code, 200)
def test_config(self):
result = self.client.get('/config')
self.assertEqual(result.status_code, 200)
json_result = result.get_json()
self.assertTrue(json_result)

114
tests/unit/test_webutils.py Normal file
View File

@@ -0,0 +1,114 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
import mock
from parameterized.parameterized import parameterized
from searx import webutils
from tests import SearxTestCase
class TestWebUtils(SearxTestCase):
@parameterized.expand(
[
('https://searx.me/', 'https://searx.me/'),
('https://searx.me/ű', 'https://searx.me/ű'),
('https://searx.me/' + (100 * 'a'), 'https://searx.me/[...]aaaaaaaaaaaaaaaaa'),
('https://searx.me/' + (100 * 'ű'), 'https://searx.me/[...]űűűűűűűűűűűűűűűűű'),
]
)
def test_prettify_url(self, test_url: str, expected: str):
self.assertEqual(webutils.prettify_url(test_url, max_length=32), expected)
@parameterized.expand(
[
(0, None, None),
(None, None, None),
('', None, None),
(False, None, None),
]
)
def test_highlight_content_none(self, content, query, expected):
self.assertEqual(webutils.highlight_content(content, query), expected)
def test_highlight_content_same(self):
content = '<html></html>not<'
self.assertEqual(webutils.highlight_content(content, None), content)
@parameterized.expand(
[
('test', 'a', 'a'),
('a test', 'a', '<span class="highlight">a</span>'),
('" test "', 'a test string', 'a <span class="highlight">test</span> string'),
('"a"', 'this is a test string', 'this is <span class="highlight">a</span> test string'),
(
'a test',
'this is a test string that matches entire query',
'this is <span class="highlight">a</span>'
' <span class="highlight">test</span>'
' string that matches entire query',
),
(
'this a test',
'this is a string to test.',
(
'<span class="highlight">this</span>'
' is <span class="highlight">a</span>'
' string to <span class="highlight">test</span>.'
),
),
(
'match this "exact phrase"',
'this string contains the exact phrase we want to match',
''.join(
[
'<span class="highlight">this</span> string contains the <span class="highlight">exact</span> ',
'<span class="highlight">phrase</span> we want to <span class="highlight">match</span>',
]
),
),
(
'a class',
'a string with class.',
'<span class="highlight">a</span> string with <span class="highlight">class</span>.',
),
]
)
def test_highlight_content_equal(self, query: str, content: str, expected: str):
self.assertEqual(webutils.highlight_content(content, query), expected)
class TestUnicodeWriter(SearxTestCase):
def setUp(self):
super().setUp()
self.unicode_writer = webutils.CSVWriter(mock.MagicMock())
def test_write_row(self):
row = [1, 2, 3]
self.assertIsNone(self.unicode_writer.writerow(row))
def test_write_rows(self):
self.unicode_writer.writerow = mock.MagicMock()
rows = [1, 2, 3]
self.unicode_writer.writerows(rows)
self.assertEqual(self.unicode_writer.writerow.call_count, len(rows))
class TestNewHmac(SearxTestCase):
@parameterized.expand(
[
b'secret',
1,
]
)
def test_attribute_error(self, secret_key):
data = b'http://example.com'
with self.assertRaises(AttributeError):
webutils.new_hmac(secret_key, data)
def test_bytes(self):
data = b'http://example.com'
res = webutils.new_hmac('secret', data)
self.assertEqual(res, '23e2baa2404012a5cc8e4a18b4aabf0dde4cb9b56f679ddc0fd6d7c24339d819')