first commit
This commit is contained in:
92
tests/__init__.py
Normal file
92
tests/__init__.py
Normal 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
2
tests/robot/__init__.py
Normal 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
69
tests/robot/__main__.py
Normal 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()
|
||||
58
tests/robot/settings_robot.yml
Normal file
58
tests/robot/settings_robot.yml
Normal 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'
|
||||
77
tests/robot/test_webapp.py
Normal file
77
tests/robot/test_webapp.py
Normal 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
10
tests/unit/__init__.py
Normal 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")
|
||||
2
tests/unit/engines/__init__.py
Normal file
2
tests/unit/engines/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
|
||||
218
tests/unit/engines/test_command.py
Normal file
218
tests/unit/engines/test_command.py
Normal 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})
|
||||
254
tests/unit/engines/test_json_engine.py
Normal file
254
tests/unit/engines/test_json_engine.py
Normal 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'])
|
||||
136
tests/unit/engines/test_xpath.py
Normal file
136
tests/unit/engines/test_xpath.py
Normal 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'])
|
||||
2
tests/unit/network/__init__.py
Normal file
2
tests/unit/network/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
|
||||
247
tests/unit/network/test_network.py
Normal file
247
tests/unit/network/test_network.py
Normal 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()
|
||||
2
tests/unit/processors/__init__.py
Normal file
2
tests/unit/processors/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# pylint: disable=missing-module-docstring,disable=missing-class-docstring,invalid-name
|
||||
38
tests/unit/processors/test_online.py
Normal file
38
tests/unit/processors/test_online.py
Normal 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'])
|
||||
0
tests/unit/settings/empty_settings.yml
Normal file
0
tests/unit/settings/empty_settings.yml
Normal file
2
tests/unit/settings/limiter.toml
Normal file
2
tests/unit/settings/limiter.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[botdetection.ip_limit]
|
||||
link_token = true
|
||||
3
tests/unit/settings/syntaxerror_settings.yml
Normal file
3
tests/unit/settings/syntaxerror_settings.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
Test:
|
||||
"**********"
|
||||
xxx
|
||||
8
tests/unit/settings/test_result_container.yml
Normal file
8
tests/unit/settings/test_result_container.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
# This SearXNG setup is used in unit tests
|
||||
|
||||
use_default_settings:
|
||||
|
||||
engines:
|
||||
keep_only:
|
||||
- google
|
||||
- duckduckgo
|
||||
30
tests/unit/settings/test_settings.yml
Normal file
30
tests/unit/settings/test_settings.yml
Normal 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"]
|
||||
16
tests/unit/settings/test_tineye.yml
Normal file
16
tests/unit/settings/test_tineye.yml
Normal 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
|
||||
141
tests/unit/settings/user_settings.yml
Normal file
141
tests/unit/settings/user_settings.yml
Normal 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)
|
||||
14
tests/unit/settings/user_settings_keep_only.yml
Normal file
14
tests/unit/settings/user_settings_keep_only.yml
Normal 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
|
||||
10
tests/unit/settings/user_settings_remove.yml
Normal file
10
tests/unit/settings/user_settings_remove.yml
Normal 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
|
||||
15
tests/unit/settings/user_settings_remove2.yml
Normal file
15
tests/unit/settings/user_settings_remove2.yml
Normal 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
|
||||
6
tests/unit/settings/user_settings_simple.yml
Normal file
6
tests/unit/settings/user_settings_simple.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
use_default_settings: true
|
||||
server:
|
||||
secret_key: "user_secret_key"
|
||||
bind_address: "[::]"
|
||||
default_http_headers:
|
||||
Custom-Header: Custom-Value
|
||||
34
tests/unit/test_answerers.py
Normal file
34
tests/unit/test_answerers.py
Normal 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)
|
||||
101
tests/unit/test_engine_tineye.py
Normal file
101
tests/unit/test_engine_tineye.py
Normal 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'])
|
||||
76
tests/unit/test_engines_init.py
Normal file
76
tests/unit/test_engines_init.py
Normal 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"']
|
||||
)
|
||||
37
tests/unit/test_exceptions.py
Normal file
37
tests/unit/test_exceptions.py
Normal 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)
|
||||
126
tests/unit/test_external_bangs.py
Normal file
126
tests/unit/test_external_bangs.py
Normal 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
117
tests/unit/test_locales.py
Normal 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)
|
||||
93
tests/unit/test_plugin_calculator.py
Normal file
93
tests/unit/test_plugin_calculator.py
Normal 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), [])
|
||||
69
tests/unit/test_plugin_hash.py
Normal file
69
tests/unit/test_plugin_hash.py
Normal 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), [])
|
||||
71
tests/unit/test_plugin_self_info.py
Normal file
71
tests/unit/test_plugin_self_info.py
Normal 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
107
tests/unit/test_plugins.py
Normal 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)
|
||||
213
tests/unit/test_preferences.py
Normal file
213
tests/unit/test_preferences.py
Normal 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
245
tests/unit/test_query.py
Normal 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'])
|
||||
60
tests/unit/test_results.py
Normal file
60
tests/unit/test_results.py
Normal 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
120
tests/unit/test_search.py
Normal 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)
|
||||
119
tests/unit/test_settings_loader.py
Normal file
119
tests/unit/test_settings_loader.py
Normal 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
13
tests/unit/test_toml.py
Normal 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
246
tests/unit/test_utils.py
Normal 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
|
||||
40
tests/unit/test_webadapter.py
Normal file
40
tests/unit/test_webadapter.py
Normal 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
250
tests/unit/test_webapp.py
Normal 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
114
tests/unit/test_webutils.py
Normal 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')
|
||||
Reference in New Issue
Block a user