Parcourir la source

Merge pull request #210 from Cqoicebordel/unit-tests

unit tests
Adam Tauber il y a 10 ans
Parent
révision
f6db77d81e

+ 3
- 6
searx/engines/currency_convert.py Voir le fichier

@@ -13,12 +13,9 @@ def request(query, params):
13 13
     if not m:
14 14
         # wrong query
15 15
         return params
16
-    try:
17
-        ammount, from_currency, to_currency = m.groups()
18
-        ammount = float(ammount)
19
-    except:
20
-        # wrong params
21
-        return params
16
+
17
+    ammount, from_currency, to_currency = m.groups()
18
+    ammount = float(ammount)
22 19
 
23 20
     q = (from_currency + to_currency).upper()
24 21
 

+ 5
- 5
searx/engines/duckduckgo.py Voir le fichier

@@ -15,7 +15,7 @@
15 15
 
16 16
 from urllib import urlencode
17 17
 from lxml.html import fromstring
18
-from searx.utils import html_to_text
18
+from searx.engines.xpath import extract_text
19 19
 
20 20
 # engine dependent config
21 21
 categories = ['general']
@@ -28,8 +28,8 @@ url = 'https://duckduckgo.com/html?{query}&s={offset}'
28 28
 # specific xpath variables
29 29
 result_xpath = '//div[@class="results_links results_links_deep web-result"]'  # noqa
30 30
 url_xpath = './/a[@class="large"]/@href'
31
-title_xpath = './/a[@class="large"]//text()'
32
-content_xpath = './/div[@class="snippet"]//text()'
31
+title_xpath = './/a[@class="large"]'
32
+content_xpath = './/div[@class="snippet"]'
33 33
 
34 34
 
35 35
 # do search-request
@@ -64,8 +64,8 @@ def response(resp):
64 64
         if not res_url:
65 65
             continue
66 66
 
67
-        title = html_to_text(''.join(r.xpath(title_xpath)))
68
-        content = html_to_text(''.join(r.xpath(content_xpath)))
67
+        title = extract_text(r.xpath(title_xpath))
68
+        content = extract_text(r.xpath(content_xpath))
69 69
 
70 70
         # append result
71 71
         results.append({'title': title,

+ 3
- 2
searx/engines/duckduckgo_definitions.py Voir le fichier

@@ -25,9 +25,10 @@ def request(query, params):
25 25
 
26 26
 
27 27
 def response(resp):
28
-    search_res = json.loads(resp.text)
29 28
     results = []
30 29
 
30
+    search_res = json.loads(resp.text)
31
+
31 32
     content = ''
32 33
     heading = search_res.get('Heading', '')
33 34
     attributes = []
@@ -68,7 +69,7 @@ def response(resp):
68 69
             results.append({'title': heading, 'url': firstURL})
69 70
 
70 71
     # related topics
71
-    for ddg_result in search_res.get('RelatedTopics', None):
72
+    for ddg_result in search_res.get('RelatedTopics', []):
72 73
         if 'FirstURL' in ddg_result:
73 74
             suggestion = result_to_text(ddg_result.get('FirstURL', None),
74 75
                                         ddg_result.get('Text', None),

+ 3
- 5
searx/engines/faroo.py Voir le fichier

@@ -37,7 +37,7 @@ search_category = {'general': 'web',
37 37
 
38 38
 # do search-request
39 39
 def request(query, params):
40
-    offset = (params['pageno']-1) * number_of_results + 1
40
+    offset = (params['pageno'] - 1) * number_of_results + 1
41 41
     categorie = search_category.get(params['category'], 'web')
42 42
 
43 43
     if params['language'] == 'all':
@@ -45,11 +45,11 @@ def request(query, params):
45 45
     else:
46 46
         language = params['language'].split('_')[0]
47 47
 
48
-    # skip, if language is not supported
48
+    # if language is not supported, put it in english
49 49
     if language != 'en' and\
50 50
        language != 'de' and\
51 51
        language != 'zh':
52
-        return params
52
+        language = 'en'
53 53
 
54 54
     params['url'] = search_url.format(offset=offset,
55 55
                                       number_of_results=number_of_results,
@@ -69,12 +69,10 @@ def response(resp):
69 69
     # HTTP-Code 401: api-key is not valide
70 70
     if resp.status_code == 401:
71 71
         raise Exception("API key is not valide")
72
-        return []
73 72
 
74 73
     # HTTP-Code 429: rate limit exceeded
75 74
     if resp.status_code == 429:
76 75
         raise Exception("rate limit has been exceeded!")
77
-        return []
78 76
 
79 77
     results = []
80 78
 

+ 5
- 4
searx/engines/openstreetmap.py Voir le fichier

@@ -38,6 +38,9 @@ def response(resp):
38 38
 
39 39
     # parse results
40 40
     for r in json:
41
+        if 'display_name' not in r:
42
+            continue
43
+
41 44
         title = r['display_name']
42 45
         osm_type = r.get('osm_type', r.get('type'))
43 46
         url = result_base_url.format(osm_type=osm_type,
@@ -49,10 +52,8 @@ def response(resp):
49 52
         geojson = r.get('geojson')
50 53
 
51 54
         # if no geojson is found and osm_type is a node, add geojson Point
52
-        if not geojson and\
53
-           osm_type == 'node':
54
-            geojson = {u'type': u'Point',
55
-                       u'coordinates': [r['lon'], r['lat']]}
55
+        if not geojson and osm_type == 'node':
56
+            geojson = {u'type': u'Point', u'coordinates': [r['lon'], r['lat']]}
56 57
 
57 58
         address_raw = r.get('address')
58 59
         address = {}

+ 1
- 1
searx/engines/photon.py Voir le fichier

@@ -61,7 +61,7 @@ def response(resp):
61 61
             continue
62 62
 
63 63
         # get title
64
-        title = properties['name']
64
+        title = properties.get('name')
65 65
 
66 66
         # get osm-type
67 67
         if properties.get('osm_type') == 'N':

+ 5
- 8
searx/engines/startpage.py Voir le fichier

@@ -13,6 +13,7 @@
13 13
 from lxml import html
14 14
 from cgi import escape
15 15
 import re
16
+from searx.engines.xpath import extract_text
16 17
 
17 18
 # engine dependent config
18 19
 categories = ['general']
@@ -45,8 +46,7 @@ def request(query, params):
45 46
 
46 47
     # set language if specified
47 48
     if params['language'] != 'all':
48
-        params['data']['with_language'] = ('lang_' +
49
-                                           params['language'].split('_')[0])
49
+        params['data']['with_language'] = ('lang_' + params['language'].split('_')[0])
50 50
 
51 51
     return params
52 52
 
@@ -64,18 +64,15 @@ def response(resp):
64 64
             continue
65 65
         link = links[0]
66 66
         url = link.attrib.get('href')
67
-        try:
68
-            title = escape(link.text_content())
69
-        except UnicodeDecodeError:
70
-            continue
71 67
 
72 68
         # block google-ad url's
73 69
         if re.match("^http(s|)://www.google.[a-z]+/aclk.*$", url):
74 70
             continue
75 71
 
72
+        title = escape(extract_text(link))
73
+
76 74
         if result.xpath('./p[@class="desc"]'):
77
-            content = escape(result.xpath('./p[@class="desc"]')[0]
78
-                             .text_content())
75
+            content = escape(extract_text(result.xpath('./p[@class="desc"]')))
79 76
         else:
80 77
             content = ''
81 78
 

+ 8
- 7
searx/engines/subtitleseeker.py Voir le fichier

@@ -12,6 +12,7 @@ from cgi import escape
12 12
 from urllib import quote_plus
13 13
 from lxml import html
14 14
 from searx.languages import language_codes
15
+from searx.engines.xpath import extract_text
15 16
 
16 17
 # engine dependent config
17 18
 categories = ['videos']
@@ -20,7 +21,7 @@ language = ""
20 21
 
21 22
 # search-url
22 23
 url = 'http://www.subtitleseeker.com/'
23
-search_url = url+'search/TITLES/{query}&p={pageno}'
24
+search_url = url + 'search/TITLES/{query}&p={pageno}'
24 25
 
25 26
 # specific xpath variables
26 27
 results_xpath = '//div[@class="boxRows"]'
@@ -44,7 +45,7 @@ def response(resp):
44 45
     if resp.search_params['language'] != 'all':
45 46
         search_lang = [lc[1]
46 47
                        for lc in language_codes
47
-                       if lc[0][:2] == resp.search_params['language']][0]
48
+                       if lc[0][:2] == resp.search_params['language'].split('_')[0]][0]
48 49
 
49 50
     # parse results
50 51
     for result in dom.xpath(results_xpath):
@@ -56,17 +57,17 @@ def response(resp):
56 57
         elif search_lang:
57 58
             href = href + search_lang + '/'
58 59
 
59
-        title = escape(link.xpath(".//text()")[0])
60
+        title = escape(extract_text(link))
60 61
 
61
-        content = result.xpath('.//div[contains(@class,"red")]//text()')[0]
62
+        content = extract_text(result.xpath('.//div[contains(@class,"red")]'))
62 63
         content = content + " - "
63
-        text = result.xpath('.//div[contains(@class,"grey-web")]')[0]
64
-        content = content + html.tostring(text, method='text')
64
+        text = extract_text(result.xpath('.//div[contains(@class,"grey-web")]')[0])
65
+        content = content + text
65 66
 
66 67
         if result.xpath(".//span") != []:
67 68
             content = content +\
68 69
                 " - (" +\
69
-                result.xpath(".//span//text()")[0].strip() +\
70
+                extract_text(result.xpath(".//span")) +\
70 71
                 ")"
71 72
 
72 73
         # append result

+ 8
- 5
searx/engines/twitter.py Voir le fichier

@@ -13,8 +13,8 @@
13 13
 from urlparse import urljoin
14 14
 from urllib import urlencode
15 15
 from lxml import html
16
-from cgi import escape
17 16
 from datetime import datetime
17
+from searx.engines.xpath import extract_text
18 18
 
19 19
 # engine dependent config
20 20
 categories = ['social media']
@@ -22,12 +22,12 @@ language_support = True
22 22
 
23 23
 # search-url
24 24
 base_url = 'https://twitter.com/'
25
-search_url = base_url+'search?'
25
+search_url = base_url + 'search?'
26 26
 
27 27
 # specific xpath variables
28 28
 results_xpath = '//li[@data-item-type="tweet"]'
29 29
 link_xpath = './/small[@class="time"]//a'
30
-title_xpath = './/span[@class="username js-action-profile-name"]//text()'
30
+title_xpath = './/span[@class="username js-action-profile-name"]'
31 31
 content_xpath = './/p[@class="js-tweet-text tweet-text"]'
32 32
 timestamp_xpath = './/span[contains(@class,"_timestamp")]'
33 33
 
@@ -39,6 +39,8 @@ def request(query, params):
39 39
     # set language if specified
40 40
     if params['language'] != 'all':
41 41
         params['cookies']['lang'] = params['language'].split('_')[0]
42
+    else:
43
+        params['cookies']['lang'] = 'en'
42 44
 
43 45
     return params
44 46
 
@@ -53,8 +55,9 @@ def response(resp):
53 55
     for tweet in dom.xpath(results_xpath):
54 56
         link = tweet.xpath(link_xpath)[0]
55 57
         url = urljoin(base_url, link.attrib.get('href'))
56
-        title = ''.join(tweet.xpath(title_xpath))
57
-        content = escape(html.tostring(tweet.xpath(content_xpath)[0], method='text', encoding='UTF-8').decode("utf-8"))
58
+        title = extract_text(tweet.xpath(title_xpath))
59
+        content = extract_text(tweet.xpath(content_xpath)[0])
60
+
58 61
         pubdate = tweet.xpath(timestamp_xpath)
59 62
         if len(pubdate) > 0:
60 63
             timestamp = float(pubdate[0].attrib.get('data-time'))

+ 11
- 8
searx/engines/yacy.py Voir le fichier

@@ -25,10 +25,10 @@ number_of_results = 5
25 25
 # search-url
26 26
 base_url = 'http://localhost:8090'
27 27
 search_url = '/yacysearch.json?{query}'\
28
-                             '&startRecord={offset}'\
29
-                             '&maximumRecords={limit}'\
30
-                             '&contentdom={search_type}'\
31
-                             '&resource=global'             # noqa
28
+             '&startRecord={offset}'\
29
+             '&maximumRecords={limit}'\
30
+             '&contentdom={search_type}'\
31
+             '&resource=global'
32 32
 
33 33
 # yacy specific type-definitions
34 34
 search_types = {'general': 'text',
@@ -41,7 +41,7 @@ search_types = {'general': 'text',
41 41
 # do search-request
42 42
 def request(query, params):
43 43
     offset = (params['pageno'] - 1) * number_of_results
44
-    search_type = search_types.get(params['category'], '0')
44
+    search_type = search_types.get(params.get('category'), '0')
45 45
 
46 46
     params['url'] = base_url +\
47 47
         search_url.format(query=urlencode({'query': query}),
@@ -66,9 +66,12 @@ def response(resp):
66 66
     if not raw_search_results:
67 67
         return []
68 68
 
69
-    search_results = raw_search_results.get('channels', {})[0].get('items', [])
69
+    search_results = raw_search_results.get('channels', [])
70 70
 
71
-    for result in search_results:
71
+    if len(search_results) == 0:
72
+        return []
73
+
74
+    for result in search_results[0].get('items', []):
72 75
         # parse image results
73 76
         if result.get('image'):
74 77
             # append result
@@ -88,7 +91,7 @@ def response(resp):
88 91
                             'content': result['description'],
89 92
                             'publishedDate': publishedDate})
90 93
 
91
-        #TODO parse video, audio and file results
94
+        # TODO parse video, audio and file results
92 95
 
93 96
     # return results
94 97
     return results

+ 2
- 2
searx/engines/yahoo.py Voir le fichier

@@ -35,7 +35,7 @@ suggestion_xpath = '//div[@id="satat"]//a'
35 35
 def parse_url(url_string):
36 36
     endings = ['/RS', '/RK']
37 37
     endpositions = []
38
-    start = url_string.find('http', url_string.find('/RU=')+1)
38
+    start = url_string.find('http', url_string.find('/RU=') + 1)
39 39
 
40 40
     for ending in endings:
41 41
         endpos = url_string.rfind(ending)
@@ -91,7 +91,7 @@ def response(resp):
91 91
                         'content': content})
92 92
 
93 93
     # if no suggestion found, return results
94
-    if not suggestion_xpath:
94
+    if not dom.xpath(suggestion_xpath):
95 95
         return results
96 96
 
97 97
     # parse suggestion

+ 44
- 0
searx/tests/engines/test_currency_convert.py Voir le fichier

@@ -0,0 +1,44 @@
1
+from collections import defaultdict
2
+from datetime import datetime
3
+import mock
4
+from searx.engines import currency_convert
5
+from searx.testing import SearxTestCase
6
+
7
+
8
+class TestCurrencyConvertEngine(SearxTestCase):
9
+
10
+    def test_request(self):
11
+        query = 'test_query'
12
+        dicto = defaultdict(dict)
13
+        dicto['pageno'] = 1
14
+        params = currency_convert.request(query, dicto)
15
+        self.assertNotIn('url', params)
16
+
17
+        query = '1.1.1 EUR in USD'
18
+        params = currency_convert.request(query, dicto)
19
+        self.assertNotIn('url', params)
20
+
21
+        query = '10 eur in usd'
22
+        params = currency_convert.request(query, dicto)
23
+        self.assertIn('url', params)
24
+        self.assertIn('finance.yahoo.com', params['url'])
25
+        self.assertIn('EUR', params['url'])
26
+        self.assertIn('USD', params['url'])
27
+
28
+    def test_response(self):
29
+        dicto = defaultdict(dict)
30
+        dicto['ammount'] = 10
31
+        dicto['from'] = "EUR"
32
+        dicto['to'] = "USD"
33
+        response = mock.Mock(text='a,b,c,d', search_params=dicto)
34
+        self.assertEqual(currency_convert.response(response), [])
35
+
36
+        csv = "2,0.5,1"
37
+        response = mock.Mock(text=csv, search_params=dicto)
38
+        results = currency_convert.response(response)
39
+        self.assertEqual(type(results), list)
40
+        self.assertEqual(len(results), 1)
41
+        self.assertEqual(results[0]['answer'], '10 EUR = 5.0 USD (1 EUR = 0.5 USD)')
42
+        now_date = datetime.now().strftime('%Y%m%d')
43
+        self.assertEqual(results[0]['url'], 'http://finance.yahoo.com/currency/converter-results/' +
44
+                                            now_date + '/10-eur-to-usd.html')

+ 90
- 0
searx/tests/engines/test_duckduckgo.py Voir le fichier

@@ -0,0 +1,90 @@
1
+from collections import defaultdict
2
+import mock
3
+from searx.engines import duckduckgo
4
+from searx.testing import SearxTestCase
5
+
6
+
7
+class TestDuckduckgoEngine(SearxTestCase):
8
+
9
+    def test_request(self):
10
+        query = 'test_query'
11
+        dicto = defaultdict(dict)
12
+        dicto['pageno'] = 1
13
+        dicto['language'] = 'fr_FR'
14
+        params = duckduckgo.request(query, dicto)
15
+        self.assertIn('url', params)
16
+        self.assertIn(query, params['url'])
17
+        self.assertIn('duckduckgo.com', params['url'])
18
+        self.assertIn('fr-fr', params['url'])
19
+
20
+        dicto['language'] = 'all'
21
+        params = duckduckgo.request(query, dicto)
22
+        self.assertIn('en-us', params['url'])
23
+
24
+    def test_response(self):
25
+        self.assertRaises(AttributeError, duckduckgo.response, None)
26
+        self.assertRaises(AttributeError, duckduckgo.response, [])
27
+        self.assertRaises(AttributeError, duckduckgo.response, '')
28
+        self.assertRaises(AttributeError, duckduckgo.response, '[]')
29
+
30
+        response = mock.Mock(text='<html></html>')
31
+        self.assertEqual(duckduckgo.response(response), [])
32
+
33
+        html = """
34
+        <div class="results_links results_links_deep web-result">
35
+            <div class="icon_fav" style="display: block;">
36
+                <a rel="nofollow" href="https://www.test.com/">
37
+                    <img width="16" height="16" alt=""
38
+                    src="/i/www.test.com.ico" style="visibility: visible;" name="i15" />
39
+                </a>
40
+            </div>
41
+            <div class="links_main links_deep"> <!-- This is the visible part -->
42
+                <a rel="nofollow" class="large" href="http://this.should.be.the.link/">
43
+                    This <b>is</b> <b>the</b> title
44
+                </a>
45
+                <div class="snippet"><b>This</b> should be the content.</div>
46
+                <div class="url">
47
+                    http://this.should.be.the.link/
48
+                </div>
49
+            </div>
50
+        </div>
51
+        """
52
+        response = mock.Mock(text=html)
53
+        results = duckduckgo.response(response)
54
+        self.assertEqual(type(results), list)
55
+        self.assertEqual(len(results), 1)
56
+        self.assertEqual(results[0]['title'], 'This is the title')
57
+        self.assertEqual(results[0]['url'], 'http://this.should.be.the.link/')
58
+        self.assertEqual(results[0]['content'], 'This should be the content.')
59
+
60
+        html = """
61
+        <div class="results_links results_links_deep web-result">
62
+            <div class="icon_fav" style="display: block;">
63
+            </div>
64
+            <div class="links_main links_deep"> <!-- This is the visible part -->
65
+                <div class="snippet"><b>This</b> should be the content.</div>
66
+                <div class="url">
67
+                    http://this.should.be.the.link/
68
+                </div>
69
+            </div>
70
+        </div>
71
+        <div class="results_links results_links_deep web-result">
72
+            <div class="icon_fav" style="display: block;">
73
+                <img width="16" height="16" alt=""
74
+                src="/i/www.test.com.ico" style="visibility: visible;" name="i15" />
75
+            </div>
76
+            <div class="links_main links_deep"> <!-- This is the visible part -->
77
+                <a rel="nofollow" class="large" href="">
78
+                    This <b>is</b> <b>the</b> title
79
+                </a>
80
+                <div class="snippet"><b>This</b> should be the content.</div>
81
+                <div class="url">
82
+                    http://this.should.be.the.link/
83
+                </div>
84
+            </div>
85
+        </div>
86
+        """
87
+        response = mock.Mock(text=html)
88
+        results = duckduckgo.response(response)
89
+        self.assertEqual(type(results), list)
90
+        self.assertEqual(len(results), 0)

+ 250
- 0
searx/tests/engines/test_duckduckgo_definitions.py Voir le fichier

@@ -0,0 +1,250 @@
1
+from collections import defaultdict
2
+import mock
3
+from searx.engines import duckduckgo_definitions
4
+from searx.testing import SearxTestCase
5
+
6
+
7
+class TestDDGDefinitionsEngine(SearxTestCase):
8
+
9
+    def test_result_to_text(self):
10
+        url = ''
11
+        text = 'Text'
12
+        html_result = 'Html'
13
+        result = duckduckgo_definitions.result_to_text(url, text, html_result)
14
+        self.assertEqual(result, text)
15
+
16
+        html_result = '<a href="url">Text in link</a>'
17
+        result = duckduckgo_definitions.result_to_text(url, text, html_result)
18
+        self.assertEqual(result, 'Text in link')
19
+
20
+    def test_request(self):
21
+        query = 'test_query'
22
+        dicto = defaultdict(dict)
23
+        dicto['pageno'] = 1
24
+        params = duckduckgo_definitions.request(query, dicto)
25
+        self.assertIn('url', params)
26
+        self.assertIn(query, params['url'])
27
+        self.assertIn('duckduckgo.com', params['url'])
28
+
29
+    def test_response(self):
30
+        self.assertRaises(AttributeError, duckduckgo_definitions.response, None)
31
+        self.assertRaises(AttributeError, duckduckgo_definitions.response, [])
32
+        self.assertRaises(AttributeError, duckduckgo_definitions.response, '')
33
+        self.assertRaises(AttributeError, duckduckgo_definitions.response, '[]')
34
+
35
+        response = mock.Mock(text='{}')
36
+        self.assertEqual(duckduckgo_definitions.response(response), [])
37
+
38
+        response = mock.Mock(text='{"data": []}')
39
+        self.assertEqual(duckduckgo_definitions.response(response), [])
40
+
41
+        json = """
42
+        {
43
+          "DefinitionSource": "definition source",
44
+          "Heading": "heading",
45
+          "ImageWidth": 0,
46
+          "RelatedTopics": [
47
+            {
48
+              "Result": "Top-level domains",
49
+              "Icon": {
50
+                "URL": "",
51
+                "Height": "",
52
+                "Width": ""
53
+              },
54
+              "FirstURL": "https://first.url",
55
+              "Text": "text"
56
+            },
57
+            {
58
+              "Topics": [
59
+                {
60
+                  "Result": "result topic",
61
+                  "Icon": {
62
+                    "URL": "",
63
+                    "Height": "",
64
+                    "Width": ""
65
+                  },
66
+                  "FirstURL": "https://duckduckgo.com/?q=2%2F2",
67
+                  "Text": "result topic text"
68
+                }
69
+              ],
70
+              "Name": "name"
71
+            }
72
+          ],
73
+          "Entity": "Entity",
74
+          "Type": "A",
75
+          "Redirect": "",
76
+          "DefinitionURL": "http://definition.url",
77
+          "AbstractURL": "https://abstract.url",
78
+          "Definition": "this is the definition",
79
+          "AbstractSource": "abstract source",
80
+          "Infobox": {
81
+            "content": [
82
+              {
83
+                "data_type": "string",
84
+                "value": "1999",
85
+                "label": "Introduced",
86
+                "wiki_order": 0
87
+              }
88
+            ],
89
+            "meta": [
90
+              {
91
+                "data_type": "string",
92
+                "value": ".test",
93
+                "label": "article_title"
94
+              }
95
+            ]
96
+          },
97
+          "Image": "image.png",
98
+          "ImageIsLogo": 0,
99
+          "Abstract": "abstract",
100
+          "AbstractText": "abstract text",
101
+          "AnswerType": "",
102
+          "ImageHeight": 0,
103
+          "Results": [{
104
+                 "Result" : "result title",
105
+                 "Icon" : {
106
+                    "URL" : "result url",
107
+                    "Height" : 16,
108
+                    "Width" : 16
109
+                 },
110
+                 "FirstURL" : "result first url",
111
+                 "Text" : "result text"
112
+              }
113
+          ],
114
+          "Answer": "answer"
115
+        }
116
+        """
117
+        response = mock.Mock(text=json)
118
+        results = duckduckgo_definitions.response(response)
119
+        self.assertEqual(type(results), list)
120
+        self.assertEqual(len(results), 4)
121
+        self.assertEqual(results[0]['answer'], 'answer')
122
+        self.assertEqual(results[1]['title'], 'heading')
123
+        self.assertEqual(results[1]['url'], 'result first url')
124
+        self.assertEqual(results[2]['suggestion'], 'text')
125
+        self.assertEqual(results[3]['infobox'], 'heading')
126
+        self.assertEqual(results[3]['id'], 'http://definition.url')
127
+        self.assertEqual(results[3]['entity'], 'Entity')
128
+        self.assertIn('abstract', results[3]['content'])
129
+        self.assertIn('this is the definition', results[3]['content'])
130
+        self.assertEqual(results[3]['img_src'], 'image.png')
131
+        self.assertIn('Introduced', results[3]['attributes'][0]['label'])
132
+        self.assertIn('1999', results[3]['attributes'][0]['value'])
133
+        self.assertIn({'url': 'https://abstract.url', 'title': 'abstract source'}, results[3]['urls'])
134
+        self.assertIn({'url': 'http://definition.url', 'title': 'definition source'}, results[3]['urls'])
135
+        self.assertIn({'name': 'name', 'suggestions': ['result topic text']}, results[3]['relatedTopics'])
136
+
137
+        json = """
138
+        {
139
+          "DefinitionSource": "definition source",
140
+          "Heading": "heading",
141
+          "ImageWidth": 0,
142
+          "RelatedTopics": [],
143
+          "Entity": "Entity",
144
+          "Type": "A",
145
+          "Redirect": "",
146
+          "DefinitionURL": "",
147
+          "AbstractURL": "https://abstract.url",
148
+          "Definition": "",
149
+          "AbstractSource": "abstract source",
150
+          "Image": "",
151
+          "ImageIsLogo": 0,
152
+          "Abstract": "",
153
+          "AbstractText": "abstract text",
154
+          "AnswerType": "",
155
+          "ImageHeight": 0,
156
+          "Results": [],
157
+          "Answer": ""
158
+        }
159
+        """
160
+        response = mock.Mock(text=json)
161
+        results = duckduckgo_definitions.response(response)
162
+        self.assertEqual(type(results), list)
163
+        self.assertEqual(len(results), 1)
164
+        self.assertEqual(results[0]['url'], 'https://abstract.url')
165
+        self.assertEqual(results[0]['title'], 'heading')
166
+        self.assertEqual(results[0]['content'], '')
167
+
168
+        json = """
169
+        {
170
+          "DefinitionSource": "definition source",
171
+          "Heading": "heading",
172
+          "ImageWidth": 0,
173
+          "RelatedTopics": [
174
+            {
175
+              "Result": "Top-level domains",
176
+              "Icon": {
177
+                "URL": "",
178
+                "Height": "",
179
+                "Width": ""
180
+              },
181
+              "FirstURL": "https://first.url",
182
+              "Text": "heading"
183
+            },
184
+            {
185
+              "Name": "name"
186
+            },
187
+            {
188
+              "Topics": [
189
+                {
190
+                  "Result": "result topic",
191
+                  "Icon": {
192
+                    "URL": "",
193
+                    "Height": "",
194
+                    "Width": ""
195
+                  },
196
+                  "FirstURL": "https://duckduckgo.com/?q=2%2F2",
197
+                  "Text": "heading"
198
+                }
199
+              ],
200
+              "Name": "name"
201
+            }
202
+          ],
203
+          "Entity": "Entity",
204
+          "Type": "A",
205
+          "Redirect": "",
206
+          "DefinitionURL": "http://definition.url",
207
+          "AbstractURL": "https://abstract.url",
208
+          "Definition": "this is the definition",
209
+          "AbstractSource": "abstract source",
210
+          "Infobox": {
211
+            "meta": [
212
+              {
213
+                "data_type": "string",
214
+                "value": ".test",
215
+                "label": "article_title"
216
+              }
217
+            ]
218
+          },
219
+          "Image": "image.png",
220
+          "ImageIsLogo": 0,
221
+          "Abstract": "abstract",
222
+          "AbstractText": "abstract text",
223
+          "AnswerType": "",
224
+          "ImageHeight": 0,
225
+          "Results": [{
226
+                 "Result" : "result title",
227
+                 "Icon" : {
228
+                    "URL" : "result url",
229
+                    "Height" : 16,
230
+                    "Width" : 16
231
+                 },
232
+                 "Text" : "result text"
233
+              }
234
+          ],
235
+          "Answer": ""
236
+        }
237
+        """
238
+        response = mock.Mock(text=json)
239
+        results = duckduckgo_definitions.response(response)
240
+        self.assertEqual(type(results), list)
241
+        self.assertEqual(len(results), 1)
242
+        self.assertEqual(results[0]['infobox'], 'heading')
243
+        self.assertEqual(results[0]['id'], 'http://definition.url')
244
+        self.assertEqual(results[0]['entity'], 'Entity')
245
+        self.assertIn('abstract', results[0]['content'])
246
+        self.assertIn('this is the definition', results[0]['content'])
247
+        self.assertEqual(results[0]['img_src'], 'image.png')
248
+        self.assertIn({'url': 'https://abstract.url', 'title': 'abstract source'}, results[0]['urls'])
249
+        self.assertIn({'url': 'http://definition.url', 'title': 'definition source'}, results[0]['urls'])
250
+        self.assertIn({'name': 'name', 'suggestions': []}, results[0]['relatedTopics'])

+ 116
- 0
searx/tests/engines/test_faroo.py Voir le fichier

@@ -0,0 +1,116 @@
1
+# -*- coding: utf-8 -*-
2
+from collections import defaultdict
3
+import mock
4
+from searx.engines import faroo
5
+from searx.testing import SearxTestCase
6
+
7
+
8
+class TestFarooEngine(SearxTestCase):
9
+
10
+    def test_request(self):
11
+        query = 'test_query'
12
+        dicto = defaultdict(dict)
13
+        dicto['pageno'] = 1
14
+        dicto['language'] = 'fr_FR'
15
+        dicto['category'] = 'general'
16
+        params = faroo.request(query, dicto)
17
+        self.assertIn('url', params)
18
+        self.assertIn(query, params['url'])
19
+        self.assertIn('faroo.com', params['url'])
20
+        self.assertIn('en', params['url'])
21
+        self.assertIn('web', params['url'])
22
+
23
+        dicto['language'] = 'all'
24
+        params = faroo.request(query, dicto)
25
+        self.assertIn('en', params['url'])
26
+
27
+        dicto['language'] = 'de_DE'
28
+        params = faroo.request(query, dicto)
29
+        self.assertIn('de', params['url'])
30
+
31
+    def test_response(self):
32
+        self.assertRaises(AttributeError, faroo.response, None)
33
+        self.assertRaises(AttributeError, faroo.response, [])
34
+        self.assertRaises(AttributeError, faroo.response, '')
35
+        self.assertRaises(AttributeError, faroo.response, '[]')
36
+
37
+        response = mock.Mock(text='{}')
38
+        self.assertEqual(faroo.response(response), [])
39
+
40
+        response = mock.Mock(text='{"data": []}')
41
+        self.assertEqual(faroo.response(response), [])
42
+
43
+        response = mock.Mock(text='{"data": []}', status_code=401)
44
+        self.assertRaises(Exception, faroo.response, response)
45
+
46
+        response = mock.Mock(text='{"data": []}', status_code=429)
47
+        self.assertRaises(Exception, faroo.response, response)
48
+
49
+        json = """
50
+        {
51
+          "results": [
52
+            {
53
+              "title": "This is the title",
54
+              "kwic": "This is the content",
55
+              "content": "",
56
+              "url": "http://this.is.the.url/",
57
+              "iurl": "",
58
+              "domain": "css3test.com",
59
+              "author": "Jim Dalrymple",
60
+              "news": true,
61
+              "votes": "10",
62
+              "date": 1360622563000,
63
+              "related": []
64
+            },
65
+            {
66
+              "title": "This is the title2",
67
+              "kwic": "This is the content2",
68
+              "content": "",
69
+              "url": "http://this.is.the.url2/",
70
+              "iurl": "",
71
+              "domain": "css3test.com",
72
+              "author": "Jim Dalrymple",
73
+              "news": false,
74
+              "votes": "10",
75
+              "related": []
76
+            },
77
+            {
78
+              "title": "This is the title3",
79
+              "kwic": "This is the content3",
80
+              "content": "",
81
+              "url": "http://this.is.the.url3/",
82
+              "iurl": "http://upload.wikimedia.org/optimized.jpg",
83
+              "domain": "css3test.com",
84
+              "author": "Jim Dalrymple",
85
+              "news": false,
86
+              "votes": "10",
87
+              "related": []
88
+            }
89
+          ],
90
+          "query": "test",
91
+          "suggestions": [],
92
+          "count": 100,
93
+          "start": 1,
94
+          "length": 10,
95
+          "time": "15"
96
+        }
97
+        """
98
+        response = mock.Mock(text=json)
99
+        results = faroo.response(response)
100
+        self.assertEqual(type(results), list)
101
+        self.assertEqual(len(results), 4)
102
+        self.assertEqual(results[0]['title'], 'This is the title')
103
+        self.assertEqual(results[0]['url'], 'http://this.is.the.url/')
104
+        self.assertEqual(results[0]['content'], 'This is the content')
105
+        self.assertEqual(results[1]['title'], 'This is the title2')
106
+        self.assertEqual(results[1]['url'], 'http://this.is.the.url2/')
107
+        self.assertEqual(results[1]['content'], 'This is the content2')
108
+        self.assertEqual(results[3]['img_src'], 'http://upload.wikimedia.org/optimized.jpg')
109
+
110
+        json = """
111
+        {}
112
+        """
113
+        response = mock.Mock(text=json)
114
+        results = faroo.response(response)
115
+        self.assertEqual(type(results), list)
116
+        self.assertEqual(len(results), 0)

+ 162
- 0
searx/tests/engines/test_google.py Voir le fichier

@@ -0,0 +1,162 @@
1
+# -*- coding: utf-8 -*-
2
+from collections import defaultdict
3
+import mock
4
+import lxml
5
+from searx.engines import google
6
+from searx.testing import SearxTestCase
7
+
8
+
9
+class TestGoogleEngine(SearxTestCase):
10
+
11
+    def test_request(self):
12
+        query = 'test_query'
13
+        dicto = defaultdict(dict)
14
+        dicto['pageno'] = 1
15
+        dicto['language'] = 'fr_FR'
16
+        params = google.request(query, dicto)
17
+        self.assertIn('url', params)
18
+        self.assertIn(query, params['url'])
19
+        self.assertIn('google.com', params['url'])
20
+        self.assertIn('PREF', params['cookies'])
21
+        self.assertIn('fr', params['headers']['Accept-Language'])
22
+
23
+        dicto['language'] = 'all'
24
+        params = google.request(query, dicto)
25
+        self.assertIn('en', params['headers']['Accept-Language'])
26
+
27
+    def test_response(self):
28
+        self.assertRaises(AttributeError, google.response, None)
29
+        self.assertRaises(AttributeError, google.response, [])
30
+        self.assertRaises(AttributeError, google.response, '')
31
+        self.assertRaises(AttributeError, google.response, '[]')
32
+
33
+        response = mock.Mock(text='<html></html>')
34
+        self.assertEqual(google.response(response), [])
35
+
36
+        html = """
37
+        <li class="g">
38
+            <h3 class="r">
39
+                <a href="http://this.should.be.the.link/">
40
+                    <b>This</b> is <b>the</b> title
41
+                </a>
42
+            </h3>
43
+            <div class="s">
44
+                <div class="kv" style="margin-bottom:2px">
45
+                    <cite>
46
+                        <b>test</b>.psychologies.com/
47
+                    </cite>
48
+                    <div class="_nBb">‎
49
+                        <div style="display:inline" onclick="google.sham(this);" aria-expanded="false"
50
+                            aria-haspopup="true" tabindex="0" data-ved="0CBUQ7B0wAA">
51
+                            <span class="_O0">
52
+                            </span>
53
+                        </div>
54
+                        <div style="display:none" class="am-dropdown-menu" role="menu" tabindex="-1">
55
+                            <ul>
56
+                                <li class="_Ykb">
57
+                                    <a class="_Zkb" href="http://www.google.fr/url?url=http://webcache.googleusercontent
58
+                                        .com/search%3Fcache:R1Z_4pGXjuIJ:http://test.psychologies.com/">
59
+                                        En cache
60
+                                    </a>
61
+                                </li>
62
+                                <li class="_Ykb">
63
+                                    <a class="_Zkb" href="/search?safe=off&amp;q=related:test.psy.com/">
64
+                                        Pages similaires
65
+                                    </a>
66
+                                </li>
67
+                            </ul>
68
+                        </div>
69
+                    </div>
70
+                </div>
71
+                <span class="st">
72
+                    This should be the content.
73
+                </span>
74
+                <br>
75
+                <div class="osl">‎
76
+                    <a href="http://www.google.fr/url?url=http://test.psychologies.com/tests/">
77
+                        Test Personnalité
78
+                    </a> - ‎
79
+                    <a href="http://www.google.fr/url?url=http://test.psychologies.com/test/">
80
+                        Tests - Moi
81
+                    </a> - ‎
82
+                    <a href="http://www.google.fr/url?url=http://test.psychologies.com/test/tests-couple">
83
+                        Test Couple
84
+                    </a>
85
+                    - ‎
86
+                    <a href="http://www.google.fr/url?url=http://test.psychologies.com/tests/tests-amour">
87
+                        Test Amour
88
+                    </a>
89
+                </div>
90
+            </div>
91
+        </li>
92
+        <li class="g">
93
+            <h3 class="r">
94
+                <a href="http://www.google.com/images?q=toto">
95
+                    <b>This</b>
96
+                </a>
97
+            </h3>
98
+        </li>
99
+        <li class="g">
100
+            <h3 class="r">
101
+                <a href="http://www.google.com/search?q=toto">
102
+                    <b>This</b> is
103
+                </a>
104
+            </h3>
105
+        </li>
106
+        <li class="g">
107
+            <h3 class="r">
108
+                <a href="€">
109
+                    <b>This</b> is <b>the</b>
110
+                </a>
111
+            </h3>
112
+        </li>
113
+        <li class="g">
114
+            <h3 class="r">
115
+                <a href="/url?q=url">
116
+                    <b>This</b> is <b>the</b>
117
+                </a>
118
+            </h3>
119
+        </li>
120
+        <p class="_Bmc" style="margin:3px 8px">
121
+            <a href="/search?num=20&amp;safe=off&amp;q=t&amp;revid=1754833769&amp;sa=X&amp;ei=-&amp;ved=">
122
+                suggestion <b>title</b>
123
+            </a>
124
+        </p>
125
+        """
126
+        response = mock.Mock(text=html)
127
+        results = google.response(response)
128
+        self.assertEqual(type(results), list)
129
+        self.assertEqual(len(results), 2)
130
+        self.assertEqual(results[0]['title'], 'This is the title')
131
+        self.assertEqual(results[0]['url'], 'http://this.should.be.the.link/')
132
+        self.assertEqual(results[0]['content'], 'This should be the content.')
133
+        self.assertEqual(results[1]['suggestion'], 'suggestion title')
134
+
135
+        html = """
136
+        <li class="b_algo" u="0|5109|4755453613245655|UAGjXgIrPH5yh-o5oNHRx_3Zta87f_QO">
137
+        </li>
138
+        """
139
+        response = mock.Mock(text=html)
140
+        results = google.response(response)
141
+        self.assertEqual(type(results), list)
142
+        self.assertEqual(len(results), 0)
143
+
144
+    def test_parse_images(self):
145
+        html = """
146
+        <li>
147
+            <div>
148
+                <a href="http://www.google.com/url?q=http://this.is.the.url/">
149
+                    <img style="margin:3px 0;margin-right:6px;padding:0" height="90"
150
+                        src="https://this.is.the.image/image.jpg" width="60" align="middle" alt="" border="0">
151
+                </a>
152
+            </div>
153
+        </li>
154
+        """
155
+        dom = lxml.html.fromstring(html)
156
+        results = google.parse_images(dom)
157
+        self.assertEqual(type(results), list)
158
+        self.assertEqual(len(results), 1)
159
+        self.assertEqual(results[0]['url'], 'http://this.is.the.url/')
160
+        self.assertEqual(results[0]['title'], '')
161
+        self.assertEqual(results[0]['content'], '')
162
+        self.assertEqual(results[0]['img_src'], 'https://this.is.the.image/image.jpg')

+ 130
- 0
searx/tests/engines/test_mediawiki.py Voir le fichier

@@ -0,0 +1,130 @@
1
+# -*- coding: utf-8 -*-
2
+from collections import defaultdict
3
+import mock
4
+from searx.engines import mediawiki
5
+from searx.testing import SearxTestCase
6
+
7
+
8
+class TestMediawikiEngine(SearxTestCase):
9
+
10
+    def test_request(self):
11
+        query = 'test_query'
12
+        dicto = defaultdict(dict)
13
+        dicto['pageno'] = 1
14
+        dicto['language'] = 'fr_FR'
15
+        params = mediawiki.request(query, dicto)
16
+        self.assertIn('url', params)
17
+        self.assertIn(query, params['url'])
18
+        self.assertIn('wikipedia.org', params['url'])
19
+        self.assertIn('fr', params['url'])
20
+
21
+        dicto['language'] = 'all'
22
+        params = mediawiki.request(query, dicto)
23
+        self.assertIn('en', params['url'])
24
+
25
+        mediawiki.base_url = "http://test.url/"
26
+        mediawiki.search_url = mediawiki.base_url +\
27
+                                 'w/api.php?action=query'\
28
+                                 '&list=search'\
29
+                                 '&{query}'\
30
+                                 '&srprop=timestamp'\
31
+                                 '&format=json'\
32
+                                 '&sroffset={offset}'\
33
+                                 '&srlimit={limit}'     # noqa
34
+        params = mediawiki.request(query, dicto)
35
+        self.assertIn('test.url', params['url'])
36
+
37
+    def test_response(self):
38
+        dicto = defaultdict(dict)
39
+        dicto['language'] = 'fr'
40
+        mediawiki.base_url = "https://{language}.wikipedia.org/"
41
+
42
+        self.assertRaises(AttributeError, mediawiki.response, None)
43
+        self.assertRaises(AttributeError, mediawiki.response, [])
44
+        self.assertRaises(AttributeError, mediawiki.response, '')
45
+        self.assertRaises(AttributeError, mediawiki.response, '[]')
46
+
47
+        response = mock.Mock(text='{}', search_params=dicto)
48
+        self.assertEqual(mediawiki.response(response), [])
49
+
50
+        response = mock.Mock(text='{"data": []}', search_params=dicto)
51
+        self.assertEqual(mediawiki.response(response), [])
52
+
53
+        json = """
54
+        {
55
+            "query-continue": {
56
+                "search": {
57
+                    "sroffset": 1
58
+                }
59
+            },
60
+            "query": {
61
+                "searchinfo": {
62
+                    "totalhits": 29721
63
+                },
64
+                "search": [
65
+                    {
66
+                        "ns": 0,
67
+                        "title": "This is the title étude",
68
+                        "timestamp": "2014-12-19T17:42:52Z"
69
+                    }
70
+                ]
71
+            }
72
+        }
73
+        """
74
+        response = mock.Mock(text=json, search_params=dicto)
75
+        results = mediawiki.response(response)
76
+        self.assertEqual(type(results), list)
77
+        self.assertEqual(len(results), 1)
78
+        self.assertEqual(results[0]['title'], u'This is the title étude')
79
+        self.assertIn('fr.wikipedia.org', results[0]['url'])
80
+        self.assertIn('This_is_the_title', results[0]['url'])
81
+        self.assertIn('%C3%A9tude', results[0]['url'])
82
+        self.assertEqual(results[0]['content'], '')
83
+
84
+        json = """
85
+        {
86
+            "query-continue": {
87
+                "search": {
88
+                    "sroffset": 1
89
+                }
90
+            },
91
+            "query": {
92
+                "searchinfo": {
93
+                    "totalhits": 29721
94
+                },
95
+                "search": [
96
+                ]
97
+            }
98
+        }
99
+        """
100
+        response = mock.Mock(text=json, search_params=dicto)
101
+        results = mediawiki.response(response)
102
+        self.assertEqual(type(results), list)
103
+        self.assertEqual(len(results), 0)
104
+
105
+        json = """
106
+        {
107
+            "query-continue": {
108
+                "search": {
109
+                    "sroffset": 1
110
+                }
111
+            },
112
+            "query": {
113
+            }
114
+        }
115
+        """
116
+        response = mock.Mock(text=json, search_params=dicto)
117
+        results = mediawiki.response(response)
118
+        self.assertEqual(type(results), list)
119
+        self.assertEqual(len(results), 0)
120
+
121
+        json = """
122
+        {"toto":[
123
+            {"id":200,"name":"Artist Name",
124
+            "link":"http:\/\/www.mediawiki.com\/artist\/1217","type":"artist"}
125
+        ]}
126
+        """
127
+        response = mock.Mock(text=json, search_params=dicto)
128
+        results = mediawiki.response(response)
129
+        self.assertEqual(type(results), list)
130
+        self.assertEqual(len(results), 0)

+ 199
- 0
searx/tests/engines/test_openstreetmap.py Voir le fichier

@@ -0,0 +1,199 @@
1
+# -*- coding: utf-8 -*-
2
+from collections import defaultdict
3
+import mock
4
+from searx.engines import openstreetmap
5
+from searx.testing import SearxTestCase
6
+
7
+
8
+class TestOpenstreetmapEngine(SearxTestCase):
9
+
10
+    def test_request(self):
11
+        query = 'test_query'
12
+        dicto = defaultdict(dict)
13
+        dicto['pageno'] = 1
14
+        params = openstreetmap.request(query, dicto)
15
+        self.assertIn('url', params)
16
+        self.assertIn(query, params['url'])
17
+        self.assertIn('openstreetmap.org', params['url'])
18
+
19
+    def test_response(self):
20
+        self.assertRaises(AttributeError, openstreetmap.response, None)
21
+        self.assertRaises(AttributeError, openstreetmap.response, [])
22
+        self.assertRaises(AttributeError, openstreetmap.response, '')
23
+        self.assertRaises(AttributeError, openstreetmap.response, '[]')
24
+
25
+        response = mock.Mock(text='{}')
26
+        self.assertEqual(openstreetmap.response(response), [])
27
+
28
+        response = mock.Mock(text='{"data": []}')
29
+        self.assertEqual(openstreetmap.response(response), [])
30
+
31
+        json = """
32
+        [
33
+          {
34
+            "place_id": "127732055",
35
+            "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright",
36
+            "osm_type": "relation",
37
+            "osm_id": "7444",
38
+            "boundingbox": [
39
+              "48.8155755",
40
+              "48.902156",
41
+              "2.224122",
42
+              "2.4697602"
43
+            ],
44
+            "lat": "48.8565056",
45
+            "lon": "2.3521334",
46
+            "display_name": "This is the title",
47
+            "class": "place",
48
+            "type": "city",
49
+            "importance": 0.96893459932191,
50
+            "icon": "https://nominatim.openstreetmap.org/images/mapicons/poi_place_city.p.20.png",
51
+            "address": {
52
+              "city": "Paris",
53
+              "county": "Paris",
54
+              "state": "Île-de-France",
55
+              "country": "France",
56
+              "country_code": "fr"
57
+            },
58
+            "geojson": {
59
+              "type": "Polygon",
60
+              "coordinates": [
61
+                [
62
+                  [
63
+                    2.224122,
64
+                    48.854199
65
+                  ]
66
+                ]
67
+              ]
68
+            }
69
+          }
70
+        ]
71
+        """
72
+        response = mock.Mock(text=json)
73
+        results = openstreetmap.response(response)
74
+        self.assertEqual(type(results), list)
75
+        self.assertEqual(len(results), 1)
76
+        self.assertEqual(results[0]['title'], 'This is the title')
77
+        self.assertEqual(results[0]['url'], 'https://openstreetmap.org/relation/7444')
78
+        self.assertIn('coordinates', results[0]['geojson'])
79
+        self.assertEqual(results[0]['geojson']['coordinates'][0][0][0], 2.224122)
80
+        self.assertEqual(results[0]['geojson']['coordinates'][0][0][1], 48.854199)
81
+        self.assertEqual(results[0]['address'], None)
82
+        self.assertIn('48.8155755', results[0]['boundingbox'])
83
+        self.assertIn('48.902156', results[0]['boundingbox'])
84
+        self.assertIn('2.224122', results[0]['boundingbox'])
85
+        self.assertIn('2.4697602', results[0]['boundingbox'])
86
+
87
+        json = """
88
+        [
89
+          {
90
+            "place_id": "127732055",
91
+            "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright",
92
+            "osm_type": "relation",
93
+            "osm_id": "7444",
94
+            "boundingbox": [
95
+              "48.8155755",
96
+              "48.902156",
97
+              "2.224122",
98
+              "2.4697602"
99
+            ],
100
+            "lat": "48.8565056",
101
+            "lon": "2.3521334",
102
+            "display_name": "This is the title",
103
+            "class": "tourism",
104
+            "type": "city",
105
+            "importance": 0.96893459932191,
106
+            "icon": "https://nominatim.openstreetmap.org/images/mapicons/poi_place_city.p.20.png",
107
+            "address": {
108
+              "city": "Paris",
109
+              "county": "Paris",
110
+              "state": "Île-de-France",
111
+              "country": "France",
112
+              "country_code": "fr",
113
+              "address29": "Address"
114
+            },
115
+            "geojson": {
116
+              "type": "Polygon",
117
+              "coordinates": [
118
+                [
119
+                  [
120
+                    2.224122,
121
+                    48.854199
122
+                  ]
123
+                ]
124
+              ]
125
+            }
126
+          },
127
+          {
128
+            "place_id": "127732055",
129
+            "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright",
130
+            "osm_type": "relation",
131
+            "osm_id": "7444",
132
+            "boundingbox": [
133
+              "48.8155755",
134
+              "48.902156",
135
+              "2.224122",
136
+              "2.4697602"
137
+            ],
138
+            "lat": "48.8565056",
139
+            "lon": "2.3521334",
140
+            "display_name": "This is the title",
141
+            "class": "tourism",
142
+            "type": "city",
143
+            "importance": 0.96893459932191,
144
+            "icon": "https://nominatim.openstreetmap.org/images/mapicons/poi_place_city.p.20.png",
145
+            "address": {
146
+              "city": "Paris",
147
+              "county": "Paris",
148
+              "state": "Île-de-France",
149
+              "country": "France",
150
+              "postcode": 75000,
151
+              "country_code": "fr"
152
+            },
153
+            "geojson": {
154
+              "type": "Polygon",
155
+              "coordinates": [
156
+                [
157
+                  [
158
+                    2.224122,
159
+                    48.854199
160
+                  ]
161
+                ]
162
+              ]
163
+            }
164
+          },
165
+          {
166
+            "place_id": "127732055",
167
+            "licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://www.openstreetmap.org/copyright",
168
+            "osm_type": "node",
169
+            "osm_id": "7444",
170
+            "boundingbox": [
171
+              "48.8155755",
172
+              "48.902156",
173
+              "2.224122",
174
+              "2.4697602"
175
+            ],
176
+            "lat": "48.8565056",
177
+            "lon": "2.3521334",
178
+            "display_name": "This is the title",
179
+            "class": "tourism",
180
+            "type": "city",
181
+            "importance": 0.96893459932191,
182
+            "icon": "https://nominatim.openstreetmap.org/images/mapicons/poi_place_city.p.20.png",
183
+            "address": {
184
+              "city": "Paris",
185
+              "county": "Paris",
186
+              "state": "Île-de-France",
187
+              "country": "France",
188
+              "country_code": "fr",
189
+              "address29": "Address"
190
+            }
191
+          }
192
+        ]
193
+        """
194
+        response = mock.Mock(text=json)
195
+        results = openstreetmap.response(response)
196
+        self.assertEqual(type(results), list)
197
+        self.assertEqual(len(results), 3)
198
+        self.assertIn('48.8565056', results[2]['geojson']['coordinates'])
199
+        self.assertIn('2.3521334', results[2]['geojson']['coordinates'])

+ 166
- 0
searx/tests/engines/test_photon.py Voir le fichier

@@ -0,0 +1,166 @@
1
+# -*- coding: utf-8 -*-
2
+from collections import defaultdict
3
+import mock
4
+from searx.engines import photon
5
+from searx.testing import SearxTestCase
6
+
7
+
8
+class TestPhotonEngine(SearxTestCase):
9
+
10
+    def test_request(self):
11
+        query = 'test_query'
12
+        dicto = defaultdict(dict)
13
+        dicto['pageno'] = 1
14
+        dicto['language'] = 'all'
15
+        params = photon.request(query, dicto)
16
+        self.assertIn('url', params)
17
+        self.assertIn(query, params['url'])
18
+        self.assertIn('photon.komoot.de', params['url'])
19
+
20
+        dicto['language'] = 'all'
21
+        params = photon.request(query, dicto)
22
+        self.assertNotIn('lang', params['url'])
23
+
24
+        dicto['language'] = 'al'
25
+        params = photon.request(query, dicto)
26
+        self.assertNotIn('lang', params['url'])
27
+
28
+        dicto['language'] = 'fr'
29
+        params = photon.request(query, dicto)
30
+        self.assertIn('fr', params['url'])
31
+
32
+    def test_response(self):
33
+        self.assertRaises(AttributeError, photon.response, None)
34
+        self.assertRaises(AttributeError, photon.response, [])
35
+        self.assertRaises(AttributeError, photon.response, '')
36
+        self.assertRaises(AttributeError, photon.response, '[]')
37
+
38
+        response = mock.Mock(text='{}')
39
+        self.assertEqual(photon.response(response), [])
40
+
41
+        response = mock.Mock(text='{"data": []}')
42
+        self.assertEqual(photon.response(response), [])
43
+
44
+        json = """
45
+        {
46
+          "features": [
47
+            {
48
+              "properties": {
49
+                "osm_key": "waterway",
50
+                "extent": [
51
+                  -1.4508446,
52
+                  51.1614997,
53
+                  -1.4408036,
54
+                  51.1525635
55
+                ],
56
+                "name": "This is the title",
57
+                "state": "England",
58
+                "osm_id": 114823817,
59
+                "osm_type": "W",
60
+                "osm_value": "river",
61
+                "city": "Test Valley",
62
+                "country": "United Kingdom"
63
+              },
64
+              "type": "Feature",
65
+              "geometry": {
66
+                "type": "Point",
67
+                "coordinates": [
68
+                  -1.4458571,
69
+                  51.1576661
70
+                ]
71
+              }
72
+            },
73
+            {
74
+              "properties": {
75
+                "osm_key": "place",
76
+                "street": "Rue",
77
+                "state": "Ile-de-France",
78
+                "osm_id": 129211377,
79
+                "osm_type": "R",
80
+                "housenumber": "10",
81
+                "postcode": "75011",
82
+                "osm_value": "house",
83
+                "city": "Paris",
84
+                "country": "France"
85
+              },
86
+              "type": "Feature",
87
+              "geometry": {
88
+                "type": "Point",
89
+                "coordinates": [
90
+                  2.3725025,
91
+                  48.8654481
92
+                ]
93
+              }
94
+            },
95
+            {
96
+              "properties": {
97
+                "osm_key": "amenity",
98
+                "street": "Allée",
99
+                "name": "Bibliothèque",
100
+                "state": "Ile-de-France",
101
+                "osm_id": 1028573132,
102
+                "osm_type": "N",
103
+                "postcode": "75001",
104
+                "osm_value": "library",
105
+                "city": "Paris",
106
+                "country": "France"
107
+              },
108
+              "type": "Feature",
109
+              "geometry": {
110
+                "type": "Point",
111
+                "coordinates": [
112
+                  2.3445634,
113
+                  48.862494
114
+                ]
115
+              }
116
+            },
117
+            {
118
+              "properties": {
119
+                "osm_key": "amenity",
120
+                "osm_id": 1028573132,
121
+                "osm_type": "Y",
122
+                "postcode": "75001",
123
+                "osm_value": "library",
124
+                "city": "Paris",
125
+                "country": "France"
126
+              },
127
+              "type": "Feature",
128
+              "geometry": {
129
+                "type": "Point",
130
+                "coordinates": [
131
+                  2.3445634,
132
+                  48.862494
133
+                ]
134
+              }
135
+            },
136
+            {
137
+            }
138
+        ],
139
+          "type": "FeatureCollection"
140
+        }
141
+        """
142
+        response = mock.Mock(text=json)
143
+        results = photon.response(response)
144
+        self.assertEqual(type(results), list)
145
+        self.assertEqual(len(results), 3)
146
+        self.assertEqual(results[0]['title'], 'This is the title')
147
+        self.assertEqual(results[0]['content'], '')
148
+        self.assertEqual(results[0]['longitude'], -1.4458571)
149
+        self.assertEqual(results[0]['latitude'], 51.1576661)
150
+        self.assertIn(-1.4508446, results[0]['boundingbox'])
151
+        self.assertIn(51.1614997, results[0]['boundingbox'])
152
+        self.assertIn(-1.4408036, results[0]['boundingbox'])
153
+        self.assertIn(51.1525635, results[0]['boundingbox'])
154
+        self.assertIn('type', results[0]['geojson'])
155
+        self.assertEqual(results[0]['geojson']['type'], 'Point')
156
+        self.assertEqual(results[0]['address'], None)
157
+        self.assertEqual(results[0]['osm']['type'], 'way')
158
+        self.assertEqual(results[0]['osm']['id'], 114823817)
159
+        self.assertEqual(results[0]['url'], 'https://openstreetmap.org/way/114823817')
160
+        self.assertEqual(results[1]['osm']['type'], 'relation')
161
+        self.assertEqual(results[2]['address']['name'], u'Bibliothèque')
162
+        self.assertEqual(results[2]['address']['house_number'], None)
163
+        self.assertEqual(results[2]['address']['locality'], 'Paris')
164
+        self.assertEqual(results[2]['address']['postcode'], '75001')
165
+        self.assertEqual(results[2]['address']['country'], 'France')
166
+        self.assertEqual(results[2]['osm']['type'], 'node')

+ 140
- 0
searx/tests/engines/test_startpage.py Voir le fichier

@@ -0,0 +1,140 @@
1
+# -*- coding: utf-8 -*-
2
+from collections import defaultdict
3
+import mock
4
+from searx.engines import startpage
5
+from searx.testing import SearxTestCase
6
+
7
+
8
+class TestStartpageEngine(SearxTestCase):
9
+
10
+    def test_request(self):
11
+        query = 'test_query'
12
+        dicto = defaultdict(dict)
13
+        dicto['pageno'] = 1
14
+        dicto['language'] = 'fr_FR'
15
+        params = startpage.request(query, dicto)
16
+        self.assertIn('url', params)
17
+        self.assertIn('startpage.com', params['url'])
18
+        self.assertIn('data', params)
19
+        self.assertIn('query', params['data'])
20
+        self.assertIn(query, params['data']['query'])
21
+        self.assertIn('with_language', params['data'])
22
+        self.assertIn('lang_fr', params['data']['with_language'])
23
+
24
+        dicto['language'] = 'all'
25
+        params = startpage.request(query, dicto)
26
+        self.assertNotIn('with_language', params['data'])
27
+
28
+    def test_response(self):
29
+        self.assertRaises(AttributeError, startpage.response, None)
30
+        self.assertRaises(AttributeError, startpage.response, [])
31
+        self.assertRaises(AttributeError, startpage.response, '')
32
+        self.assertRaises(AttributeError, startpage.response, '[]')
33
+
34
+        response = mock.Mock(content='<html></html>')
35
+        self.assertEqual(startpage.response(response), [])
36
+
37
+        html = """
38
+        <div class='result' style=' *width : auto; *margin-right : 10%;'>
39
+            <h3>
40
+                <a href='http://this.should.be.the.link/' id='title_2' name='title_2' >
41
+                    This should be the title
42
+                </a>
43
+                <span id='title_stars_2' name='title_stars_2'>  </span>
44
+            </h3>
45
+            <p class='desc'>
46
+                This should be the content.
47
+            </p>
48
+            <p>
49
+                <span class='url'>www.speed<b>test</b>.net/fr/
50
+                </span>
51
+                  -
52
+                <A class="proxy" id="proxy_link" HREF="https://ixquick-proxy.com/do/spg/proxy?ep=&edata=&ek=&ekdata="
53
+                    class='proxy'>
54
+                    Navigation avec Ixquick Proxy
55
+                </A>
56
+                    -
57
+                <A HREF="https://ixquick-proxy.com/do/spg/highlight.pl?l=francais&c=hf&cat=web&q=test&rl=NONE&rid=
58
+                    &hlq=https://startpage.com/do/search&mtabp=-1&mtcmd=process_search&mtlanguage=francais&mtengine0=
59
+                    &mtcat=web&u=http:%2F%2Fwww.speedtest.net%2Ffr%2F" class='proxy'>
60
+                    Mis en surbrillance
61
+                </A>
62
+            </p>
63
+        </div>
64
+        """
65
+        response = mock.Mock(content=html)
66
+        results = startpage.response(response)
67
+        self.assertEqual(type(results), list)
68
+        self.assertEqual(len(results), 1)
69
+        self.assertEqual(results[0]['title'], 'This should be the title')
70
+        self.assertEqual(results[0]['url'], 'http://this.should.be.the.link/')
71
+        self.assertEqual(results[0]['content'], 'This should be the content.')
72
+
73
+        html = """
74
+        <div class='result' style=' *width : auto; *margin-right : 10%;'>
75
+            <h3>
76
+                <a href='http://www.google.com/aclk?sa=l&ai=C' id='title_2' name='title_2' >
77
+                    This should be the title
78
+                </a>
79
+                <span id='title_stars_2' name='title_stars_2'>  </span>
80
+            </h3>
81
+            <p class='desc'>
82
+                This should be the content.
83
+            </p>
84
+            <p>
85
+                <span class='url'>www.speed<b>test</b>.net/fr/
86
+                </span>
87
+                  -
88
+                <A class="proxy" id="proxy_link" HREF="https://ixquick-proxy.com/do/spg/proxy?ep=&edata=&ek=&ekdata="
89
+                    class='proxy'>
90
+                    Navigation avec Ixquick Proxy
91
+                </A>
92
+                    -
93
+                <A HREF="https://ixquick-proxy.com/do/spg/highlight.pl?l=francais&c=hf&cat=web&q=test&rl=NONE&rid=
94
+                    &hlq=https://startpage.com/do/search&mtabp=-1&mtcmd=process_search&mtlanguage=francais&mtengine0=
95
+                    &mtcat=web&u=http:%2F%2Fwww.speedtest.net%2Ffr%2F" class='proxy'>
96
+                    Mis en surbrillance
97
+                </A>
98
+            </p>
99
+        </div>
100
+        <div class='result' style=' *width : auto; *margin-right : 10%;'>
101
+            <h3>
102
+                <span id='title_stars_2' name='title_stars_2'>  </span>
103
+            </h3>
104
+            <p class='desc'>
105
+                This should be the content.
106
+            </p>
107
+            <p>
108
+                <span class='url'>www.speed<b>test</b>.net/fr/
109
+                </span>
110
+            </p>
111
+        </div>
112
+        <div class='result' style=' *width : auto; *margin-right : 10%;'>
113
+            <h3>
114
+                <a href='http://this.should.be.the.link/' id='title_2' name='title_2' >
115
+                    This should be the title
116
+                </a>
117
+                <span id='title_stars_2' name='title_stars_2'>  </span>
118
+            </h3>
119
+            <p>
120
+                <span class='url'>www.speed<b>test</b>.net/fr/
121
+                </span>
122
+                  -
123
+                <A class="proxy" id="proxy_link" HREF="https://ixquick-proxy.com/do/spg/proxy?ep=&edata=&ek=&ekdata="
124
+                    class='proxy'>
125
+                    Navigation avec Ixquick Proxy
126
+                </A>
127
+                    -
128
+                <A HREF="https://ixquick-proxy.com/do/spg/highlight.pl?l=francais&c=hf&cat=web&q=test&rl=NONE&rid=
129
+                    &hlq=https://startpage.com/do/search&mtabp=-1&mtcmd=process_search&mtlanguage=francais&mtengine0=
130
+                    &mtcat=web&u=http:%2F%2Fwww.speedtest.net%2Ffr%2F" class='proxy'>
131
+                    Mis en surbrillance
132
+                </A>
133
+            </p>
134
+        </div>
135
+        """
136
+        response = mock.Mock(content=html)
137
+        results = startpage.response(response)
138
+        self.assertEqual(type(results), list)
139
+        self.assertEqual(len(results), 1)
140
+        self.assertEqual(results[0]['content'], '')

+ 169
- 0
searx/tests/engines/test_subtitleseeker.py Voir le fichier

@@ -0,0 +1,169 @@
1
+from collections import defaultdict
2
+import mock
3
+from searx.engines import subtitleseeker
4
+from searx.testing import SearxTestCase
5
+
6
+
7
+class TestSubtitleseekerEngine(SearxTestCase):
8
+
9
+    def test_request(self):
10
+        query = 'test_query'
11
+        dicto = defaultdict(dict)
12
+        dicto['pageno'] = 1
13
+        params = subtitleseeker.request(query, dicto)
14
+        self.assertTrue('url' in params)
15
+        self.assertTrue(query in params['url'])
16
+        self.assertTrue('subtitleseeker.com' in params['url'])
17
+
18
+    def test_response(self):
19
+        dicto = defaultdict(dict)
20
+        dicto['language'] = 'fr_FR'
21
+        response = mock.Mock(search_params=dicto)
22
+
23
+        self.assertRaises(AttributeError, subtitleseeker.response, None)
24
+        self.assertRaises(AttributeError, subtitleseeker.response, [])
25
+        self.assertRaises(AttributeError, subtitleseeker.response, '')
26
+        self.assertRaises(AttributeError, subtitleseeker.response, '[]')
27
+
28
+        response = mock.Mock(text='<html></html>', search_params=dicto)
29
+        self.assertEqual(subtitleseeker.response(response), [])
30
+
31
+        html = """
32
+        <div class="boxRows">
33
+            <div class="boxRowsInner" style="width:600px;">
34
+                <img src="http://static.subtitleseeker.com/images/movie.gif"
35
+                    style="width:16px; height:16px;" class="icon">
36
+                <a href="http://this.is.the.url/"
37
+                    class="blue" title="Title subtitle" >
38
+                    This is the Title
39
+                </a>
40
+                <br><br>
41
+                <span class="f10b grey-dark arial" style="padding:0px 0px 5px 20px">
42
+                    "Alternative Title"
43
+                </span>
44
+            </div>
45
+            <div class="boxRowsInner f12b red" style="width:70px;">
46
+                1998
47
+            </div>
48
+            <div class="boxRowsInner grey-web f12" style="width:120px;">
49
+                <img src="http://static.subtitleseeker.com/images/basket_put.png"
50
+                    style="width:16px; height:16px;" class="icon">
51
+                1039 Subs
52
+            </div>
53
+            <div class="boxRowsInner grey-web f10" style="width:130px;">
54
+                <img src="http://static.subtitleseeker.com/images/arrow_refresh_small.png"
55
+                    style="width:16px; height:16px;" class="icon">
56
+                1 hours ago
57
+            </div>
58
+            <div class="clear"></div>
59
+        </div>
60
+        """
61
+        response = mock.Mock(text=html, search_params=dicto)
62
+        results = subtitleseeker.response(response)
63
+        self.assertEqual(type(results), list)
64
+        self.assertEqual(len(results), 1)
65
+        self.assertEqual(results[0]['title'], 'This is the Title')
66
+        self.assertEqual(results[0]['url'], 'http://this.is.the.url/French/')
67
+        self.assertIn('1998', results[0]['content'])
68
+        self.assertIn('1039 Subs', results[0]['content'])
69
+        self.assertIn('Alternative Title', results[0]['content'])
70
+
71
+        html = """
72
+        <div class="boxRows">
73
+            <div class="boxRowsInner" style="width:600px;">
74
+                <img src="http://static.subtitleseeker.com/images/movie.gif"
75
+                    style="width:16px; height:16px;" class="icon">
76
+                <a href="http://this.is.the.url/"
77
+                    class="blue" title="Title subtitle" >
78
+                    This is the Title
79
+                </a>
80
+            </div>
81
+            <div class="boxRowsInner f12b red" style="width:70px;">
82
+                1998
83
+            </div>
84
+            <div class="boxRowsInner grey-web f12" style="width:120px;">
85
+                <img src="http://static.subtitleseeker.com/images/basket_put.png"
86
+                    style="width:16px; height:16px;" class="icon">
87
+                1039 Subs
88
+            </div>
89
+            <div class="boxRowsInner grey-web f10" style="width:130px;">
90
+                <img src="http://static.subtitleseeker.com/images/arrow_refresh_small.png"
91
+                    style="width:16px; height:16px;" class="icon">
92
+                1 hours ago
93
+            </div>
94
+            <div class="clear"></div>
95
+        </div>
96
+        """
97
+        dicto['language'] = 'all'
98
+        response = mock.Mock(text=html, search_params=dicto)
99
+        results = subtitleseeker.response(response)
100
+        self.assertEqual(type(results), list)
101
+        self.assertEqual(len(results), 1)
102
+        self.assertEqual(results[0]['title'], 'This is the Title')
103
+        self.assertEqual(results[0]['url'], 'http://this.is.the.url/')
104
+        self.assertIn('1998', results[0]['content'])
105
+        self.assertIn('1039 Subs', results[0]['content'])
106
+
107
+        html = """
108
+        <div class="boxRows">
109
+            <div class="boxRowsInner" style="width:600px;">
110
+                <img src="http://static.subtitleseeker.com/images/movie.gif"
111
+                    style="width:16px; height:16px;" class="icon">
112
+                <a href="http://this.is.the.url/"
113
+                    class="blue" title="Title subtitle" >
114
+                    This is the Title
115
+                </a>
116
+            </div>
117
+            <div class="boxRowsInner f12b red" style="width:70px;">
118
+                1998
119
+            </div>
120
+            <div class="boxRowsInner grey-web f12" style="width:120px;">
121
+                <img src="http://static.subtitleseeker.com/images/basket_put.png"
122
+                    style="width:16px; height:16px;" class="icon">
123
+                1039 Subs
124
+            </div>
125
+            <div class="boxRowsInner grey-web f10" style="width:130px;">
126
+                <img src="http://static.subtitleseeker.com/images/arrow_refresh_small.png"
127
+                    style="width:16px; height:16px;" class="icon">
128
+                1 hours ago
129
+            </div>
130
+            <div class="clear"></div>
131
+        </div>
132
+        """
133
+        subtitleseeker.language = 'English'
134
+        response = mock.Mock(text=html, search_params=dicto)
135
+        results = subtitleseeker.response(response)
136
+        self.assertEqual(type(results), list)
137
+        self.assertEqual(len(results), 1)
138
+        self.assertEqual(results[0]['title'], 'This is the Title')
139
+        self.assertEqual(results[0]['url'], 'http://this.is.the.url/English/')
140
+        self.assertIn('1998', results[0]['content'])
141
+        self.assertIn('1039 Subs', results[0]['content'])
142
+
143
+        html = """
144
+        <div class="boxRowsInner" style="width:600px;">
145
+            <img src="http://static.subtitleseeker.com/images/movie.gif"
146
+                style="width:16px; height:16px;" class="icon">
147
+            <a href="http://this.is.the.url/"
148
+                class="blue" title="Title subtitle" >
149
+                This is the Title
150
+            </a>
151
+        </div>
152
+        <div class="boxRowsInner f12b red" style="width:70px;">
153
+            1998
154
+        </div>
155
+        <div class="boxRowsInner grey-web f12" style="width:120px;">
156
+            <img src="http://static.subtitleseeker.com/images/basket_put.png"
157
+                style="width:16px; height:16px;" class="icon">
158
+            1039 Subs
159
+        </div>
160
+        <div class="boxRowsInner grey-web f10" style="width:130px;">
161
+            <img src="http://static.subtitleseeker.com/images/arrow_refresh_small.png"
162
+                style="width:16px; height:16px;" class="icon">
163
+            1 hours ago
164
+        </div>
165
+        """
166
+        response = mock.Mock(text=html, search_params=dicto)
167
+        results = subtitleseeker.response(response)
168
+        self.assertEqual(type(results), list)
169
+        self.assertEqual(len(results), 0)

+ 502
- 0
searx/tests/engines/test_twitter.py Voir le fichier

@@ -0,0 +1,502 @@
1
+# -*- coding: utf-8 -*-
2
+from collections import defaultdict
3
+import mock
4
+from searx.engines import twitter
5
+from searx.testing import SearxTestCase
6
+
7
+
8
+class TestTwitterEngine(SearxTestCase):
9
+
10
+    def test_request(self):
11
+        query = 'test_query'
12
+        dicto = defaultdict(dict)
13
+        dicto['pageno'] = 0
14
+        dicto['language'] = 'fr_FR'
15
+        params = twitter.request(query, dicto)
16
+        self.assertIn('url', params)
17
+        self.assertIn(query, params['url'])
18
+        self.assertIn('twitter.com', params['url'])
19
+        self.assertIn('cookies', params)
20
+        self.assertIn('lang', params['cookies'])
21
+        self.assertIn('fr', params['cookies']['lang'])
22
+
23
+        dicto['language'] = 'all'
24
+        params = twitter.request(query, dicto)
25
+        self.assertIn('cookies', params)
26
+        self.assertIn('lang', params['cookies'])
27
+        self.assertIn('en', params['cookies']['lang'])
28
+
29
+    def test_response(self):
30
+        self.assertRaises(AttributeError, twitter.response, None)
31
+        self.assertRaises(AttributeError, twitter.response, [])
32
+        self.assertRaises(AttributeError, twitter.response, '')
33
+        self.assertRaises(AttributeError, twitter.response, '[]')
34
+
35
+        response = mock.Mock(text='<html></html>')
36
+        self.assertEqual(twitter.response(response), [])
37
+
38
+        html = """
39
+        <li class="js-stream-item stream-item stream-item expanding-stream-item" data-item-id="563005573290287105"
40
+            id="stream-item-tweet-563005573290287105" data-item-type="tweet">
41
+            <div class="tweet original-tweet js-stream-tweet js-actionable-tweet js-profile-popup-actionable
42
+                js-original-tweet has-cards has-native-media" data-tweet-id="563005573290287105" data-disclosure-type=""
43
+                data-item-id="563005573290287105" data-screen-name="Jalopnik" data-name="Jalopnik"
44
+                data-user-id="3060631" data-has-native-media="true" data-has-cards="true" data-card-type="photo"
45
+                data-expanded-footer="&lt;div class=&quot;js-tweet-details-fixer
46
+                tweet-details-fixer&quot;&gt;&#10;&#10;&#10;
47
+                &lt;div class=&quot;cards-media-container js-media-container&quot;&gt;&lt;div
48
+                data-card-url=&quot;//twitter.com/Jalopnik/status/563005573290287105/photo/1&quot; data-card-type=&quot;
49
+                photo&quot; class=&quot;cards-base cards-multimedia&quot; data-element-context=&quot;platform_photo_card
50
+                &quot;&gt;&#10;&#10;&#10;  &lt;a class=&quot;media media-thumbnail twitter-timeline-link is-preview
51
+                &quot; data-url=&quot;https://pbs.twimg.com/media/B9Aylf5IMAAuziP.jpg:large&quot;
52
+                data-resolved-url-large=&quot;https://pbs.twimg.com/media/B9Aylf5IMAAuziP.jpg:large&quot;
53
+                href=&quot;//twitter.com/Jalopnik/status/563005573290287105/photo/1&quot;&gt;&#10;
54
+                &lt;div class=&quot;&quot;&gt;&#10; &lt;img src=&quot;
55
+                https://pbs.twimg.com/media/B9Aylf5IMAAuziP.jpg&quot;
56
+                alt=&quot;Embedded image permalink&quot; width=&quot;636&quot; height=&quot;309&quot;&gt;&#10;
57
+                &lt;/div&gt;&#10;&#10;  &lt;/a&gt;&#10;&#10;  &lt;div class=&quot;cards-content&quot;&gt;&#10;
58
+                &lt;div class=&quot;byline&quot;&gt;&#10;      &#10;    &lt;/div&gt;&#10;    &#10;  &lt;/div&gt;&#10;
59
+                &#10;&lt;/div&gt;&#10;&#10;&#10;&#10;&#10;&lt;/div&gt;&#10;&#10;&#10;&#10;  &lt;div
60
+                class=&quot;js-machine-translated-tweet-container&quot;&gt;&lt;/div&gt;&#10;    &lt;div
61
+                class=&quot;js-tweet-stats-container tweet-stats-container &quot;&gt;&#10;    &lt;/div&gt;&#10;&#10;
62
+                &lt;div class=&quot;client-and-actions&quot;&gt;&#10;  &lt;span class=&quot;metadata&quot;&gt;&#10;
63
+                &lt;span&gt;5:06 PM - 4 Feb 2015&lt;/span&gt;&#10;&#10;       &amp;middot; &lt;a
64
+                class=&quot;permalink-link js-permalink js-nav&quot; href=&quot;/Jalopnik/status/563005573290287105
65
+                &quot;tabindex=&quot;-1&quot;&gt;Details&lt;/a&gt;&#10;    &#10;&#10;        &#10;        &#10;
66
+                &#10;&#10;  &lt;/span&gt;&#10;&lt;/div&gt;&#10;&#10;&#10;&lt;/div&gt;&#10;" data-you-follow="false"
67
+                data-you-block="false">
68
+                <div class="context">
69
+                </div>
70
+                <div class="content">
71
+                    <div class="stream-item-header">
72
+                        <a class="account-group js-account-group js-action-profile js-user-profile-link js-nav"
73
+                            href="/Jalopnik" data-user-id="3060631">
74
+                            <img class="avatar js-action-profile-avatar"
75
+                                src="https://pbs.twimg.com/profile_images/2976430168/5cd4a59_bigger.jpeg" alt="">
76
+                            <strong class="fullname js-action-profile-name show-popup-with-id" data-aria-label-part>
77
+                                Jalopnik
78
+                            </strong>
79
+                            <span>&rlm;</span>
80
+                            <span class="username js-action-profile-name" data-aria-label-part>
81
+                            <s>@</s><b>TitleName</b>
82
+                            </span>
83
+                        </a>
84
+                        <small class="time">
85
+                        <a href="/this.is.the.url"
86
+                            class="tweet-timestamp js-permalink js-nav js-tooltip" title="5:06 PM - 4 Feb 2015" >
87
+                            <span class="u-hiddenVisually" data-aria-label-part="last">17 minutes ago</span>
88
+                        </a>
89
+                        </small>
90
+                    </div>
91
+                    <p class="js-tweet-text tweet-text" lang="en" data-aria-label-part="0">
92
+                        This is the content étude à€
93
+                        <a href="http://t.co/nRWsqQAwBL" rel="nofollow" dir="ltr"
94
+                            data-expanded-url="http://jalo.ps/ReMENu4" class="twitter-timeline-link"
95
+                            target="_blank" title="http://jalo.ps/ReMENu4" >
96
+                        <span class="tco-ellipsis">
97
+                        </span>
98
+                        <span class="invisible">http://</span><span class="js-display-url">link.in.tweet</span>
99
+                        <span class="invisible"></span>
100
+                        <span class="tco-ellipsis">
101
+                            <span class="invisible">&nbsp;</span>
102
+                        </span>
103
+                    </a>
104
+                    <a href="http://t.co/rbFsfeE0l3" class="twitter-timeline-link u-hidden"
105
+                        data-pre-embedded="true" dir="ltr">
106
+                        pic.twitter.com/rbFsfeE0l3
107
+                    </a>
108
+                    </p>
109
+                    <div class="expanded-content js-tweet-details-dropdown">
110
+                    </div>
111
+                    <div class="stream-item-footer">
112
+                        <a class="details with-icn js-details" href="/Jalopnik/status/563005573290287105">
113
+                            <span class="Icon Icon--photo">
114
+                            </span>
115
+                            <b>
116
+                                <span class="expand-stream-item js-view-details">
117
+                                    View photo
118
+                                </span>
119
+                                <span class="collapse-stream-item  js-hide-details">
120
+                                    Hide photo
121
+                                </span>
122
+                            </b>
123
+                        </a>
124
+                        <span class="ProfileTweet-action--reply u-hiddenVisually">
125
+                            <span class="ProfileTweet-actionCount" aria-hidden="true" data-tweet-stat-count="0">
126
+                                <span class="ProfileTweet-actionCountForAria" >0 replies</span>
127
+                            </span>
128
+                        </span>
129
+                        <span class="ProfileTweet-action--retweet u-hiddenVisually">
130
+                            <span class="ProfileTweet-actionCount"  data-tweet-stat-count="8">
131
+                                <span class="ProfileTweet-actionCountForAria" data-aria-label-part>8 retweets</span>
132
+                            </span>
133
+                        </span>
134
+                        <span class="ProfileTweet-action--favorite u-hiddenVisually">
135
+                            <span class="ProfileTweet-actionCount"  data-tweet-stat-count="14">
136
+                                <span class="ProfileTweet-actionCountForAria" data-aria-label-part>14 favorites</span>
137
+                            </span>
138
+                        </span>
139
+                        <div role="group" aria-label="Tweet actions" class="ProfileTweet-actionList u-cf js-actions">
140
+                            <div class="ProfileTweet-action ProfileTweet-action--reply">
141
+                                <button class="ProfileTweet-actionButton u-textUserColorHover js-actionButton
142
+                                    js-actionReply" data-modal="ProfileTweet-reply" type="button" title="Reply">
143
+                                    <span class="Icon Icon--reply">
144
+                                    </span>
145
+                                    <span class="u-hiddenVisually">Reply</span>
146
+                                    <span class="ProfileTweet-actionCount u-textUserColorHover
147
+                                        ProfileTweet-actionCount--isZero">
148
+                                        <span class="ProfileTweet-actionCountForPresentation" aria-hidden="true">
149
+                                        </span>
150
+                                    </span>
151
+                                </button>
152
+                            </div>
153
+                            <div class="ProfileTweet-action ProfileTweet-action--retweet js-toggleState js-toggleRt">
154
+                                <button class="ProfileTweet-actionButton  js-actionButton js-actionRetweet js-tooltip"
155
+                                    title="Retweet" data-modal="ProfileTweet-retweet" type="button">
156
+                                    <span class="Icon Icon--retweet">
157
+                                    </span>
158
+                                    <span class="u-hiddenVisually">Retweet</span>
159
+                                    <span class="ProfileTweet-actionCount">
160
+                                        <span class="ProfileTweet-actionCountForPresentation">8</span>
161
+                                    </span>
162
+                                </button>
163
+                                <button class="ProfileTweet-actionButtonUndo js-actionButton js-actionRetweet"
164
+                                    data-modal="ProfileTweet-retweet" title="Undo retweet" type="button">
165
+                                    <span class="Icon Icon--retweet">
166
+                                    </span>
167
+                                    <span class="u-hiddenVisually">Retweeted</span>
168
+                                    <span class="ProfileTweet-actionCount">
169
+                                        <span class="ProfileTweet-actionCountForPresentation">8</span>
170
+                                    </span>
171
+                                </button>
172
+                            </div>
173
+                            <div class="ProfileTweet-action ProfileTweet-action--favorite js-toggleState">
174
+                                <button class="ProfileTweet-actionButton js-actionButton js-actionFavorite js-tooltip"
175
+                                    title="Favorite" type="button">
176
+                                    <span class="Icon Icon--favorite">
177
+                                    </span>
178
+                                    <span class="u-hiddenVisually">Favorite</span>
179
+                                    <span class="ProfileTweet-actionCount">
180
+                                        <span class="ProfileTweet-actionCountForPresentation">14</span>
181
+                                    </span>
182
+                                </button>
183
+                                <button class="ProfileTweet-actionButtonUndo u-linkClean js-actionButton
184
+                                    js-actionFavorite" title="Undo favorite" type="button">
185
+                                    <span class="Icon Icon--favorite">
186
+                                    </span>
187
+                                    <span class="u-hiddenVisually">Favorited</span>
188
+                                    <span class="ProfileTweet-actionCount">
189
+                                        <span class="ProfileTweet-actionCountForPresentation">
190
+                                            14
191
+                                        </span>
192
+                                    </span>
193
+                                </button>
194
+                            </div>
195
+                            <div class="ProfileTweet-action ProfileTweet-action--more js-more-ProfileTweet-actions">
196
+                                <div class="dropdown">
197
+                                    <button class="ProfileTweet-actionButton u-textUserColorHover dropdown-toggle
198
+                                        js-tooltip js-dropdown-toggle" type="button" title="More">
199
+                                        <span class="Icon Icon--dots">
200
+                                        </span>
201
+                                        <span class="u-hiddenVisually">More</span>
202
+                                    </button>
203
+                                    <div class="dropdown-menu">
204
+                                        <div class="dropdown-caret">
205
+                                            <div class="caret-outer">
206
+                                            </div>
207
+                                            <div class="caret-inner">
208
+                                            </div>
209
+                                        </div>
210
+                                        <ul>
211
+                                            <li class="share-via-dm js-actionShareViaDM" data-nav="share_tweet_dm">
212
+                                                <button type="button" class="dropdown-link">
213
+                                                    Share via Direct Message
214
+                                                </button>
215
+                                            </li>
216
+                                            <li class="embed-link js-actionEmbedTweet" data-nav="embed_tweet">
217
+                                                <button type="button" class="dropdown-link">
218
+                                                    Embed Tweet
219
+                                                </button>
220
+                                            </li>
221
+                                            <li class="mute-user-item pretty-link">
222
+                                                <button type="button" class="dropdown-link">
223
+                                                    Mute
224
+                                                </button>
225
+                                            </li>
226
+                                            <li class="unmute-user-item pretty-link">
227
+                                                <button type="button" class="dropdown-link">
228
+                                                    Unmute
229
+                                                </button>
230
+                                            </li>
231
+                                            <li class="block-or-report-link js-actionBlockOrReport"
232
+                                                data-nav="block_or_report">
233
+                                                <button type="button" class="dropdown-link">
234
+                                                    Block or report
235
+                                                </button>
236
+                                            </li>
237
+                                        </ul>
238
+                                    </div>
239
+                                </div>
240
+                            </div>
241
+                        </div>
242
+                    </div>
243
+                </div>
244
+            </div>
245
+        </li>
246
+        """
247
+        response = mock.Mock(text=html)
248
+        results = twitter.response(response)
249
+        self.assertEqual(type(results), list)
250
+        self.assertEqual(len(results), 1)
251
+        self.assertEqual(results[0]['title'], '@TitleName')
252
+        self.assertEqual(results[0]['url'], 'https://twitter.com/this.is.the.url')
253
+        self.assertIn(u'This is the content', results[0]['content'])
254
+        # self.assertIn(u'This is the content étude à€', results[0]['content'])
255
+
256
+        html = """
257
+        <li class="js-stream-item stream-item stream-item expanding-stream-item" data-item-id="563005573290287105"
258
+            id="stream-item-tweet-563005573290287105" data-item-type="tweet">
259
+            <div class="tweet original-tweet js-stream-tweet js-actionable-tweet js-profile-popup-actionable
260
+                js-original-tweet has-cards has-native-media" data-tweet-id="563005573290287105" data-disclosure-type=""
261
+                data-item-id="563005573290287105" data-screen-name="Jalopnik" data-name="Jalopnik"
262
+                data-user-id="3060631" data-has-native-media="true" data-has-cards="true" data-card-type="photo"
263
+                data-expanded-footer="&lt;div class=&quot;js-tweet-details-fixer
264
+                tweet-details-fixer&quot;&gt;&#10;&#10;&#10;
265
+                &lt;div class=&quot;cards-media-container js-media-container&quot;&gt;&lt;div
266
+                data-card-url=&quot;//twitter.com/Jalopnik/status/563005573290287105/photo/1&quot; data-card-type=&quot;
267
+                photo&quot; class=&quot;cards-base cards-multimedia&quot; data-element-context=&quot;platform_photo_card
268
+                &quot;&gt;&#10;&#10;&#10;  &lt;a class=&quot;media media-thumbnail twitter-timeline-link is-preview
269
+                &quot; data-url=&quot;https://pbs.twimg.com/media/B9Aylf5IMAAuziP.jpg:large&quot;
270
+                data-resolved-url-large=&quot;https://pbs.twimg.com/media/B9Aylf5IMAAuziP.jpg:large&quot;
271
+                href=&quot;//twitter.com/Jalopnik/status/563005573290287105/photo/1&quot;&gt;&#10;
272
+                &lt;div class=&quot;&quot;&gt;&#10; &lt;img src=&quot;
273
+                https://pbs.twimg.com/media/B9Aylf5IMAAuziP.jpg&quot;
274
+                alt=&quot;Embedded image permalink&quot; width=&quot;636&quot; height=&quot;309&quot;&gt;&#10;
275
+                &lt;/div&gt;&#10;&#10;  &lt;/a&gt;&#10;&#10;  &lt;div class=&quot;cards-content&quot;&gt;&#10;
276
+                &lt;div class=&quot;byline&quot;&gt;&#10;      &#10;    &lt;/div&gt;&#10;    &#10;  &lt;/div&gt;&#10;
277
+                &#10;&lt;/div&gt;&#10;&#10;&#10;&#10;&#10;&lt;/div&gt;&#10;&#10;&#10;&#10;  &lt;div
278
+                class=&quot;js-machine-translated-tweet-container&quot;&gt;&lt;/div&gt;&#10;    &lt;div
279
+                class=&quot;js-tweet-stats-container tweet-stats-container &quot;&gt;&#10;    &lt;/div&gt;&#10;&#10;
280
+                &lt;div class=&quot;client-and-actions&quot;&gt;&#10;  &lt;span class=&quot;metadata&quot;&gt;&#10;
281
+                &lt;span&gt;5:06 PM - 4 Feb 2015&lt;/span&gt;&#10;&#10;       &amp;middot; &lt;a
282
+                class=&quot;permalink-link js-permalink js-nav&quot; href=&quot;/Jalopnik/status/563005573290287105
283
+                &quot;tabindex=&quot;-1&quot;&gt;Details&lt;/a&gt;&#10;    &#10;&#10;        &#10;        &#10;
284
+                &#10;&#10;  &lt;/span&gt;&#10;&lt;/div&gt;&#10;&#10;&#10;&lt;/div&gt;&#10;" data-you-follow="false"
285
+                data-you-block="false">
286
+                <div class="context">
287
+                </div>
288
+                <div class="content">
289
+                    <div class="stream-item-header">
290
+                        <a class="account-group js-account-group js-action-profile js-user-profile-link js-nav"
291
+                            href="/Jalopnik" data-user-id="3060631">
292
+                            <img class="avatar js-action-profile-avatar"
293
+                                src="https://pbs.twimg.com/profile_images/2976430168/5cd4a59_bigger.jpeg" alt="">
294
+                            <strong class="fullname js-action-profile-name show-popup-with-id" data-aria-label-part>
295
+                                Jalopnik
296
+                            </strong>
297
+                            <span>&rlm;</span>
298
+                            <span class="username js-action-profile-name" data-aria-label-part>
299
+                            <s>@</s><b>TitleName</b>
300
+                            </span>
301
+                        </a>
302
+                        <small class="time">
303
+                        <a href="/this.is.the.url"
304
+                            class="tweet-timestamp js-permalink js-nav js-tooltip" title="5:06 PM - 4 Feb 2015" >
305
+                            <span class="_timestamp js-short-timestamp js-relative-timestamp"  data-time="1423065963"
306
+                                data-time-ms="1423065963000" data-long-form="true" aria-hidden="true">
307
+                                17m
308
+                            </span>
309
+                            <span class="u-hiddenVisually" data-aria-label-part="last">17 minutes ago</span>
310
+                        </a>
311
+                        </small>
312
+                    </div>
313
+                    <p class="js-tweet-text tweet-text" lang="en" data-aria-label-part="0">
314
+                        This is the content étude à€
315
+                        <a href="http://t.co/nRWsqQAwBL" rel="nofollow" dir="ltr"
316
+                            data-expanded-url="http://jalo.ps/ReMENu4" class="twitter-timeline-link"
317
+                            target="_blank" title="http://jalo.ps/ReMENu4" >
318
+                        <span class="tco-ellipsis">
319
+                        </span>
320
+                        <span class="invisible">http://</span><span class="js-display-url">link.in.tweet</span>
321
+                        <span class="invisible"></span>
322
+                        <span class="tco-ellipsis">
323
+                            <span class="invisible">&nbsp;</span>
324
+                        </span>
325
+                    </a>
326
+                    <a href="http://t.co/rbFsfeE0l3" class="twitter-timeline-link u-hidden"
327
+                        data-pre-embedded="true" dir="ltr">
328
+                        pic.twitter.com/rbFsfeE0l3
329
+                    </a>
330
+                    </p>
331
+                    <div class="expanded-content js-tweet-details-dropdown">
332
+                    </div>
333
+                    <div class="stream-item-footer">
334
+                        <a class="details with-icn js-details" href="/Jalopnik/status/563005573290287105">
335
+                            <span class="Icon Icon--photo">
336
+                            </span>
337
+                            <b>
338
+                                <span class="expand-stream-item js-view-details">
339
+                                    View photo
340
+                                </span>
341
+                                <span class="collapse-stream-item  js-hide-details">
342
+                                    Hide photo
343
+                                </span>
344
+                            </b>
345
+                        </a>
346
+                        <span class="ProfileTweet-action--reply u-hiddenVisually">
347
+                            <span class="ProfileTweet-actionCount" aria-hidden="true" data-tweet-stat-count="0">
348
+                                <span class="ProfileTweet-actionCountForAria" >0 replies</span>
349
+                            </span>
350
+                        </span>
351
+                        <span class="ProfileTweet-action--retweet u-hiddenVisually">
352
+                            <span class="ProfileTweet-actionCount"  data-tweet-stat-count="8">
353
+                                <span class="ProfileTweet-actionCountForAria" data-aria-label-part>8 retweets</span>
354
+                            </span>
355
+                        </span>
356
+                        <span class="ProfileTweet-action--favorite u-hiddenVisually">
357
+                            <span class="ProfileTweet-actionCount"  data-tweet-stat-count="14">
358
+                                <span class="ProfileTweet-actionCountForAria" data-aria-label-part>14 favorites</span>
359
+                            </span>
360
+                        </span>
361
+                        <div role="group" aria-label="Tweet actions" class="ProfileTweet-actionList u-cf js-actions">
362
+                            <div class="ProfileTweet-action ProfileTweet-action--reply">
363
+                                <button class="ProfileTweet-actionButton u-textUserColorHover js-actionButton
364
+                                    js-actionReply" data-modal="ProfileTweet-reply" type="button" title="Reply">
365
+                                    <span class="Icon Icon--reply">
366
+                                    </span>
367
+                                    <span class="u-hiddenVisually">Reply</span>
368
+                                    <span class="ProfileTweet-actionCount u-textUserColorHover
369
+                                        ProfileTweet-actionCount--isZero">
370
+                                        <span class="ProfileTweet-actionCountForPresentation" aria-hidden="true">
371
+                                        </span>
372
+                                    </span>
373
+                                </button>
374
+                            </div>
375
+                            <div class="ProfileTweet-action ProfileTweet-action--retweet js-toggleState js-toggleRt">
376
+                                <button class="ProfileTweet-actionButton  js-actionButton js-actionRetweet js-tooltip"
377
+                                    title="Retweet" data-modal="ProfileTweet-retweet" type="button">
378
+                                    <span class="Icon Icon--retweet">
379
+                                    </span>
380
+                                    <span class="u-hiddenVisually">Retweet</span>
381
+                                    <span class="ProfileTweet-actionCount">
382
+                                        <span class="ProfileTweet-actionCountForPresentation">8</span>
383
+                                    </span>
384
+                                </button>
385
+                                <button class="ProfileTweet-actionButtonUndo js-actionButton js-actionRetweet"
386
+                                    data-modal="ProfileTweet-retweet" title="Undo retweet" type="button">
387
+                                    <span class="Icon Icon--retweet">
388
+                                    </span>
389
+                                    <span class="u-hiddenVisually">Retweeted</span>
390
+                                    <span class="ProfileTweet-actionCount">
391
+                                        <span class="ProfileTweet-actionCountForPresentation">8</span>
392
+                                    </span>
393
+                                </button>
394
+                            </div>
395
+                            <div class="ProfileTweet-action ProfileTweet-action--favorite js-toggleState">
396
+                                <button class="ProfileTweet-actionButton js-actionButton js-actionFavorite js-tooltip"
397
+                                    title="Favorite" type="button">
398
+                                    <span class="Icon Icon--favorite">
399
+                                    </span>
400
+                                    <span class="u-hiddenVisually">Favorite</span>
401
+                                    <span class="ProfileTweet-actionCount">
402
+                                        <span class="ProfileTweet-actionCountForPresentation">14</span>
403
+                                    </span>
404
+                                </button>
405
+                                <button class="ProfileTweet-actionButtonUndo u-linkClean js-actionButton
406
+                                    js-actionFavorite" title="Undo favorite" type="button">
407
+                                    <span class="Icon Icon--favorite">
408
+                                    </span>
409
+                                    <span class="u-hiddenVisually">Favorited</span>
410
+                                    <span class="ProfileTweet-actionCount">
411
+                                        <span class="ProfileTweet-actionCountForPresentation">
412
+                                            14
413
+                                        </span>
414
+                                    </span>
415
+                                </button>
416
+                            </div>
417
+                            <div class="ProfileTweet-action ProfileTweet-action--more js-more-ProfileTweet-actions">
418
+                                <div class="dropdown">
419
+                                    <button class="ProfileTweet-actionButton u-textUserColorHover dropdown-toggle
420
+                                        js-tooltip js-dropdown-toggle" type="button" title="More">
421
+                                        <span class="Icon Icon--dots">
422
+                                        </span>
423
+                                        <span class="u-hiddenVisually">More</span>
424
+                                    </button>
425
+                                    <div class="dropdown-menu">
426
+                                        <div class="dropdown-caret">
427
+                                            <div class="caret-outer">
428
+                                            </div>
429
+                                            <div class="caret-inner">
430
+                                            </div>
431
+                                        </div>
432
+                                        <ul>
433
+                                            <li class="share-via-dm js-actionShareViaDM" data-nav="share_tweet_dm">
434
+                                                <button type="button" class="dropdown-link">
435
+                                                    Share via Direct Message
436
+                                                </button>
437
+                                            </li>
438
+                                            <li class="embed-link js-actionEmbedTweet" data-nav="embed_tweet">
439
+                                                <button type="button" class="dropdown-link">
440
+                                                    Embed Tweet
441
+                                                </button>
442
+                                            </li>
443
+                                            <li class="mute-user-item pretty-link">
444
+                                                <button type="button" class="dropdown-link">
445
+                                                    Mute
446
+                                                </button>
447
+                                            </li>
448
+                                            <li class="unmute-user-item pretty-link">
449
+                                                <button type="button" class="dropdown-link">
450
+                                                    Unmute
451
+                                                </button>
452
+                                            </li>
453
+                                            <li class="block-or-report-link js-actionBlockOrReport"
454
+                                                data-nav="block_or_report">
455
+                                                <button type="button" class="dropdown-link">
456
+                                                    Block or report
457
+                                                </button>
458
+                                            </li>
459
+                                        </ul>
460
+                                    </div>
461
+                                </div>
462
+                            </div>
463
+                        </div>
464
+                    </div>
465
+                </div>
466
+            </div>
467
+        </li>
468
+        """
469
+        response = mock.Mock(text=html)
470
+        results = twitter.response(response)
471
+        self.assertEqual(type(results), list)
472
+        self.assertEqual(len(results), 1)
473
+        self.assertEqual(results[0]['title'], '@TitleName')
474
+        self.assertEqual(results[0]['url'], 'https://twitter.com/this.is.the.url')
475
+        self.assertIn(u'This is the content', results[0]['content'])
476
+
477
+        html = """
478
+        <li class="b_algo" u="0|5109|4755453613245655|UAGjXgIrPH5yh-o5oNHRx_3Zta87f_QO">
479
+            <div Class="sa_mc">
480
+                <div class="sb_tlst">
481
+                    <h2>
482
+                        <a href="http://this.should.be.the.link/" h="ID=SERP,5124.1">
483
+                        <strong>This</strong> should be the title</a>
484
+                    </h2>
485
+                </div>
486
+                <div class="sb_meta">
487
+                <cite>
488
+                <strong>this</strong>.meta.com</cite>
489
+                    <span class="c_tlbxTrg">
490
+                        <span class="c_tlbxH" H="BASE:CACHEDPAGEDEFAULT" K="SERP,5125.1">
491
+                        </span>
492
+                    </span>
493
+                </div>
494
+                <p>
495
+                <strong>This</strong> should be the content.</p>
496
+            </div>
497
+        </li>
498
+        """
499
+        response = mock.Mock(text=html)
500
+        results = twitter.response(response)
501
+        self.assertEqual(type(results), list)
502
+        self.assertEqual(len(results), 0)

+ 96
- 0
searx/tests/engines/test_yacy.py Voir le fichier

@@ -0,0 +1,96 @@
1
+from collections import defaultdict
2
+import mock
3
+from searx.engines import yacy
4
+from searx.testing import SearxTestCase
5
+
6
+
7
+class TestYacyEngine(SearxTestCase):
8
+
9
+    def test_request(self):
10
+        query = 'test_query'
11
+        dicto = defaultdict(dict)
12
+        dicto['pageno'] = 1
13
+        dicto['language'] = 'fr_FR'
14
+        params = yacy.request(query, dicto)
15
+        self.assertIn('url', params)
16
+        self.assertIn(query, params['url'])
17
+        self.assertIn('localhost', params['url'])
18
+        self.assertIn('fr', params['url'])
19
+
20
+        dicto['language'] = 'all'
21
+        params = yacy.request(query, dicto)
22
+        self.assertIn('url', params)
23
+        self.assertNotIn('lr=lang_', params['url'])
24
+
25
+    def test_response(self):
26
+        self.assertRaises(AttributeError, yacy.response, None)
27
+        self.assertRaises(AttributeError, yacy.response, [])
28
+        self.assertRaises(AttributeError, yacy.response, '')
29
+        self.assertRaises(AttributeError, yacy.response, '[]')
30
+
31
+        response = mock.Mock(text='{}')
32
+        self.assertEqual(yacy.response(response), [])
33
+
34
+        response = mock.Mock(text='{"data": []}')
35
+        self.assertEqual(yacy.response(response), [])
36
+
37
+        json = """
38
+        {
39
+          "channels": [
40
+            {
41
+              "title": "YaCy P2P-Search for test",
42
+              "description": "Search for test",
43
+              "link": "http://search.yacy.de:7001/yacysearch.html?query=test&amp;resource=global&amp;contentdom=0",
44
+              "image": {
45
+                "url": "http://search.yacy.de:7001/env/grafics/yacy.png",
46
+                "title": "Search for test",
47
+                "link": "http://search.yacy.de:7001/yacysearch.html?query=test&amp;resource=global&amp;contentdom=0"
48
+              },
49
+              "totalResults": "249",
50
+              "startIndex": "0",
51
+              "itemsPerPage": "5",
52
+              "searchTerms": "test",
53
+              "items": [
54
+                {
55
+                  "title": "This is the title",
56
+                  "link": "http://this.is.the.url",
57
+                  "code": "",
58
+                  "description": "This should be the content",
59
+                  "pubDate": "Sat, 08 Jun 2013 02:00:00 +0200",
60
+                  "size": "44213",
61
+                  "sizename": "43 kbyte",
62
+                  "guid": "lzh_1T_5FP-A",
63
+                  "faviconCode": "XTS4uQ_5FP-A",
64
+                  "host": "www.gamestar.de",
65
+                  "path": "/spiele/city-of-heroes-freedom/47019.html",
66
+                  "file": "47019.html",
67
+                  "urlhash": "lzh_1T_5FP-A",
68
+                  "ranking": "0.20106804"
69
+                },
70
+                {
71
+                  "title": "This is the title2",
72
+                  "icon": "/ViewImage.png?maxwidth=96&amp;maxheight=96&amp;code=7EbAbW6BpPOA",
73
+                  "image": "http://image.url/image.png",
74
+                  "cache": "/ViewImage.png?quadratic=&amp;url=http://golem.ivwbox.de/cgi-bin/ivw/CP/G_INET?d=14071378",
75
+                  "url": "http://this.is.the.url",
76
+                  "urlhash": "7EbAbW6BpPOA",
77
+                  "host": "www.golem.de",
78
+                  "width": "-1",
79
+                  "height": "-1"
80
+                }
81
+              ]
82
+            }
83
+          ]
84
+        }
85
+        """
86
+        response = mock.Mock(text=json)
87
+        results = yacy.response(response)
88
+        self.assertEqual(type(results), list)
89
+        self.assertEqual(len(results), 2)
90
+        self.assertEqual(results[0]['title'], 'This is the title')
91
+        self.assertEqual(results[0]['url'], 'http://this.is.the.url')
92
+        self.assertEqual(results[0]['content'], 'This should be the content')
93
+        self.assertEqual(results[1]['img_src'], 'http://image.url/image.png')
94
+        self.assertEqual(results[1]['content'], '')
95
+        self.assertEqual(results[1]['url'], 'http://this.is.the.url')
96
+        self.assertEqual(results[1]['title'], 'This is the title2')

+ 154
- 0
searx/tests/engines/test_yahoo.py Voir le fichier

@@ -0,0 +1,154 @@
1
+# -*- coding: utf-8 -*-
2
+from collections import defaultdict
3
+import mock
4
+from searx.engines import yahoo
5
+from searx.testing import SearxTestCase
6
+
7
+
8
+class TestYahooEngine(SearxTestCase):
9
+
10
+    def test_parse_url(self):
11
+        test_url = 'http://r.search.yahoo.com/_ylt=A0LEb9JUSKcAEGRXNyoA;_ylu=X3oDMTEzZm1qazYwBHNlYwNzcgRwb3MDMQRjb' +\
12
+                   '2xvA2Jm2dGlkA1NNRTcwM18x/RV=2/RE=1423106085/RO=10/RU=https%3a%2f%2fthis.is.the.url%2f/RK=0/RS=' +\
13
+                   'dtcJsfP4mEeBOjnVfUQ-'
14
+        url = yahoo.parse_url(test_url)
15
+        self.assertEqual('https://this.is.the.url/', url)
16
+
17
+        test_url = 'http://r.search.yahoo.com/_ylt=A0LElb9JUSKcAEGRXNyoA;_ylu=X3oDMTEzZm1qazYwBHNlYwNzcgRwb3MDMQRjb' +\
18
+                   '2xvA2Jm2dGlkA1NNRTcwM18x/RV=2/RE=1423106085/RO=10/RU=https%3a%2f%2fthis.is.the.url%2f/RS=' +\
19
+                   'dtcJsfP4mEeBOjnVfUQ-'
20
+        url = yahoo.parse_url(test_url)
21
+        self.assertEqual('https://this.is.the.url/', url)
22
+
23
+        test_url = 'https://this.is.the.url/'
24
+        url = yahoo.parse_url(test_url)
25
+        self.assertEqual('https://this.is.the.url/', url)
26
+
27
+    def test_request(self):
28
+        query = 'test_query'
29
+        dicto = defaultdict(dict)
30
+        dicto['pageno'] = 1
31
+        dicto['language'] = 'fr_FR'
32
+        params = yahoo.request(query, dicto)
33
+        self.assertIn('url', params)
34
+        self.assertIn(query, params['url'])
35
+        self.assertIn('search.yahoo.com', params['url'])
36
+        self.assertIn('fr', params['url'])
37
+        self.assertIn('cookies', params)
38
+        self.assertIn('sB', params['cookies'])
39
+        self.assertIn('fr', params['cookies']['sB'])
40
+
41
+        dicto['language'] = 'all'
42
+        params = yahoo.request(query, dicto)
43
+        self.assertIn('cookies', params)
44
+        self.assertIn('sB', params['cookies'])
45
+        self.assertIn('en', params['cookies']['sB'])
46
+        self.assertIn('en', params['url'])
47
+
48
+    def test_response(self):
49
+        self.assertRaises(AttributeError, yahoo.response, None)
50
+        self.assertRaises(AttributeError, yahoo.response, [])
51
+        self.assertRaises(AttributeError, yahoo.response, '')
52
+        self.assertRaises(AttributeError, yahoo.response, '[]')
53
+
54
+        response = mock.Mock(text='<html></html>')
55
+        self.assertEqual(yahoo.response(response), [])
56
+
57
+        html = """
58
+        <div class="res">
59
+            <div>
60
+                <h3>
61
+                <a id="link-1" class="yschttl spt" href="http://r.search.yahoo.com/_ylt=A0LEVzClb9JUSKcAEGRXNyoA;
62
+                    _ylu=X3oDMTEzZm1qazYwBHNlYwNzcgRwb3MDMQRjb2xvA2JmMQR2dGlkA1NNRTcwM18x/RV=2/RE=1423106085/RO=10
63
+                    /RU=https%3a%2f%2fthis.is.the.url%2f/RK=0/RS=dtcJsfP4mEeBOjnVfUQ-"target="_blank" data-bk="5063.1">
64
+                    <b>This</b> is the title
65
+                </a>
66
+                </h3>
67
+            </div>
68
+            <span class="url" dir="ltr">www.<b>test</b>.com</span>
69
+            <div class="abstr">
70
+                <b>This</b> is the content
71
+            </div>
72
+        </div>
73
+        <div id="satat"  data-bns="Yahoo" data-bk="124.1">
74
+            <h2>Also Try</h2>
75
+            <table>
76
+                <tbody>
77
+                    <tr>
78
+                        <td>
79
+                            <a id="srpnat0" class="" href="https://search.yahoo.com/search=rs-bottom" >
80
+                                <span>
81
+                                    <b></b>This is <b>the suggestion</b>
82
+                                </span>
83
+                            </a>
84
+                        </td>
85
+                    </tr>
86
+                </tbody>
87
+            </table>
88
+        </div>
89
+        """
90
+        response = mock.Mock(text=html)
91
+        results = yahoo.response(response)
92
+        self.assertEqual(type(results), list)
93
+        self.assertEqual(len(results), 2)
94
+        self.assertEqual(results[0]['title'], 'This is the title')
95
+        self.assertEqual(results[0]['url'], 'https://this.is.the.url/')
96
+        self.assertEqual(results[0]['content'], 'This is the content')
97
+        self.assertEqual(results[1]['suggestion'], 'This is the suggestion')
98
+
99
+        html = """
100
+        <div class="res">
101
+            <div>
102
+                <h3>
103
+                <a id="link-1" class="yschttl spt" href="http://r.search.yahoo.com/_ylt=A0LEVzClb9JUSKcAEGRXNyoA;
104
+                    _ylu=X3oDMTEzZm1qazYwBHNlYwNzcgRwb3MDMQRjb2xvA2JmMQR2dGlkA1NNRTcwM18x/RV=2/RE=1423106085/RO=10
105
+                    /RU=https%3a%2f%2fthis.is.the.url%2f/RK=0/RS=dtcJsfP4mEeBOjnVfUQ-"target="_blank" data-bk="5063.1">
106
+                    <b>This</b> is the title
107
+                </a>
108
+                </h3>
109
+            </div>
110
+            <span class="url" dir="ltr">www.<b>test</b>.com</span>
111
+            <div class="abstr">
112
+                <b>This</b> is the content
113
+            </div>
114
+        </div>
115
+        <div class="res">
116
+            <div>
117
+                <h3>
118
+                <a id="link-1" class="yschttl spt">
119
+                    <b>This</b> is the title
120
+                </a>
121
+                </h3>
122
+            </div>
123
+            <span class="url" dir="ltr">www.<b>test</b>.com</span>
124
+            <div class="abstr">
125
+                <b>This</b> is the content
126
+            </div>
127
+        </div>
128
+        <div class="res">
129
+            <div>
130
+                <h3>
131
+                </h3>
132
+            </div>
133
+            <span class="url" dir="ltr">www.<b>test</b>.com</span>
134
+            <div class="abstr">
135
+                <b>This</b> is the content
136
+            </div>
137
+        </div>
138
+        """
139
+        response = mock.Mock(text=html)
140
+        results = yahoo.response(response)
141
+        self.assertEqual(type(results), list)
142
+        self.assertEqual(len(results), 1)
143
+        self.assertEqual(results[0]['title'], 'This is the title')
144
+        self.assertEqual(results[0]['url'], 'https://this.is.the.url/')
145
+        self.assertEqual(results[0]['content'], 'This is the content')
146
+
147
+        html = """
148
+        <li class="b_algo" u="0|5109|4755453613245655|UAGjXgIrPH5yh-o5oNHRx_3Zta87f_QO">
149
+        </li>
150
+        """
151
+        response = mock.Mock(text=html)
152
+        results = yahoo.response(response)
153
+        self.assertEqual(type(results), list)
154
+        self.assertEqual(len(results), 0)

+ 143
- 0
searx/tests/engines/test_yahoo_news.py Voir le fichier

@@ -0,0 +1,143 @@
1
+# -*- coding: utf-8 -*-
2
+from collections import defaultdict
3
+from datetime import datetime
4
+import mock
5
+from searx.engines import yahoo_news
6
+from searx.testing import SearxTestCase
7
+
8
+
9
+class TestYahooNewsEngine(SearxTestCase):
10
+
11
+    def test_request(self):
12
+        query = 'test_query'
13
+        dicto = defaultdict(dict)
14
+        dicto['pageno'] = 1
15
+        dicto['language'] = 'fr_FR'
16
+        params = yahoo_news.request(query, dicto)
17
+        self.assertIn('url', params)
18
+        self.assertIn(query, params['url'])
19
+        self.assertIn('news.search.yahoo.com', params['url'])
20
+        self.assertIn('fr', params['url'])
21
+        self.assertIn('cookies', params)
22
+        self.assertIn('sB', params['cookies'])
23
+        self.assertIn('fr', params['cookies']['sB'])
24
+
25
+        dicto['language'] = 'all'
26
+        params = yahoo_news.request(query, dicto)
27
+        self.assertIn('cookies', params)
28
+        self.assertIn('sB', params['cookies'])
29
+        self.assertIn('en', params['cookies']['sB'])
30
+        self.assertIn('en', params['url'])
31
+
32
+    def test_response(self):
33
+        self.assertRaises(AttributeError, yahoo_news.response, None)
34
+        self.assertRaises(AttributeError, yahoo_news.response, [])
35
+        self.assertRaises(AttributeError, yahoo_news.response, '')
36
+        self.assertRaises(AttributeError, yahoo_news.response, '[]')
37
+
38
+        response = mock.Mock(text='<html></html>')
39
+        self.assertEqual(yahoo_news.response(response), [])
40
+
41
+        html = """
42
+        <div class="res">
43
+            <div>
44
+                <h3>
45
+                    <a class="yschttl spt" href="http://this.is.the.url" target="_blank">
46
+                        This is
47
+                        the <b>title</b>...
48
+                    </a>
49
+                </h3>
50
+            </div>
51
+            <span class="url">Business via Yahoo! Finance</span> &nbsp; <span class="timestamp">Feb 03 09:45am</span>
52
+            <div class="abstr">
53
+                This is the content
54
+            </div>
55
+        </div>
56
+        """
57
+        response = mock.Mock(text=html)
58
+        results = yahoo_news.response(response)
59
+        self.assertEqual(type(results), list)
60
+        self.assertEqual(len(results), 1)
61
+        self.assertEqual(results[0]['title'], 'This is the title...')
62
+        self.assertEqual(results[0]['url'], 'http://this.is.the.url/')
63
+        self.assertEqual(results[0]['content'], 'This is the content')
64
+
65
+        html = """
66
+        <div class="res">
67
+            <div>
68
+                <h3>
69
+                    <a class="yschttl spt" href="http://this.is.the.url" target="_blank">
70
+                        This is
71
+                        the <b>title</b>...
72
+                    </a>
73
+                </h3>
74
+            </div>
75
+            <span class="url">Business via Yahoo!</span> &nbsp; <span class="timestamp">2 hours, 22 minutes ago</span>
76
+            <div class="abstr">
77
+                This is the content
78
+            </div>
79
+        </div>
80
+        <div class="res">
81
+            <div>
82
+                <h3>
83
+                    <a class="yschttl spt" href="http://this.is.the.url" target="_blank">
84
+                        This is
85
+                        the <b>title</b>...
86
+                    </a>
87
+                </h3>
88
+            </div>
89
+            <span class="url">Business via Yahoo!</span> &nbsp; <span class="timestamp">22 minutes ago</span>
90
+            <div class="abstr">
91
+                This is the content
92
+            </div>
93
+        </div>
94
+        <div class="res">
95
+            <div>
96
+                <h3>
97
+                    <a class="yschttl spt" href="http://this.is.the.url" target="_blank">
98
+                        This is
99
+                        the <b>title</b>...
100
+                    </a>
101
+                </h3>
102
+            </div>
103
+            <span class="url">Business via Yahoo!</span> &nbsp; <span class="timestamp">Feb 03 09:45am 1900</span>
104
+            <div class="abstr">
105
+                This is the content
106
+            </div>
107
+        </div>
108
+        """
109
+        response = mock.Mock(text=html)
110
+        results = yahoo_news.response(response)
111
+        self.assertEqual(type(results), list)
112
+        self.assertEqual(len(results), 3)
113
+        self.assertEqual(results[0]['title'], 'This is the title...')
114
+        self.assertEqual(results[0]['url'], 'http://this.is.the.url/')
115
+        self.assertEqual(results[0]['content'], 'This is the content')
116
+        self.assertEqual(results[2]['publishedDate'].year, datetime.now().year)
117
+
118
+        html = """
119
+        <li class="b_algo" u="0|5109|4755453613245655|UAGjXgIrPH5yh-o5oNHRx_3Zta87f_QO">
120
+            <div Class="sa_mc">
121
+                <div class="sb_tlst">
122
+                    <h2>
123
+                        <a href="http://this.should.be.the.link/" h="ID=SERP,5124.1">
124
+                        <strong>This</strong> should be the title</a>
125
+                    </h2>
126
+                </div>
127
+                <div class="sb_meta">
128
+                <cite>
129
+                <strong>this</strong>.meta.com</cite>
130
+                    <span class="c_tlbxTrg">
131
+                        <span class="c_tlbxH" H="BASE:CACHEDPAGEDEFAULT" K="SERP,5125.1">
132
+                        </span>
133
+                    </span>
134
+                </div>
135
+                <p>
136
+                <strong>This</strong> should be the content.</p>
137
+            </div>
138
+        </li>
139
+        """
140
+        response = mock.Mock(text=html)
141
+        results = yahoo_news.response(response)
142
+        self.assertEqual(type(results), list)
143
+        self.assertEqual(len(results), 0)

+ 15
- 1
searx/tests/test_engines.py Voir le fichier

@@ -3,25 +3,39 @@ from searx.tests.engines.test_bing_images import *  # noqa
3 3
 from searx.tests.engines.test_bing_news import *  # noqa
4 4
 from searx.tests.engines.test_blekko_images import *  # noqa
5 5
 from searx.tests.engines.test_btdigg import *  # noqa
6
+from searx.tests.engines.test_currency_convert import *  # noqa
6 7
 from searx.tests.engines.test_dailymotion import *  # noqa
7 8
 from searx.tests.engines.test_deezer import *  # noqa
8 9
 from searx.tests.engines.test_deviantart import *  # noqa
9 10
 from searx.tests.engines.test_digg import *  # noqa
11
+from searx.tests.engines.test_duckduckgo import *  # noqa
12
+from searx.tests.engines.test_duckduckgo_definitions import *  # noqa
10 13
 from searx.tests.engines.test_dummy import *  # noqa
14
+from searx.tests.engines.test_faroo import *  # noqa
11 15
 from searx.tests.engines.test_flickr import *  # noqa
12 16
 from searx.tests.engines.test_flickr_noapi import *  # noqa
13 17
 from searx.tests.engines.test_gigablast import *  # noqa
14 18
 from searx.tests.engines.test_github import *  # noqa
15
-from searx.tests.engines.test_www1x import *  # noqa
19
+from searx.tests.engines.test_google import *  # noqa
16 20
 from searx.tests.engines.test_google_images import *  # noqa
17 21
 from searx.tests.engines.test_google_news import *  # noqa
18 22
 from searx.tests.engines.test_kickass import *  # noqa
23
+from searx.tests.engines.test_mediawiki import *  # noqa
19 24
 from searx.tests.engines.test_mixcloud import *  # noqa
25
+from searx.tests.engines.test_openstreetmap import *  # noqa
26
+from searx.tests.engines.test_photon import *  # noqa
20 27
 from searx.tests.engines.test_piratebay import *  # noqa
21 28
 from searx.tests.engines.test_searchcode_code import *  # noqa
22 29
 from searx.tests.engines.test_searchcode_doc import *  # noqa
23 30
 from searx.tests.engines.test_soundcloud import *  # noqa
24 31
 from searx.tests.engines.test_stackoverflow import *  # noqa
32
+from searx.tests.engines.test_startpage import *  # noqa
33
+from searx.tests.engines.test_subtitleseeker import *  # noqa
34
+from searx.tests.engines.test_twitter import *  # noqa
25 35
 from searx.tests.engines.test_vimeo import *  # noqa
36
+from searx.tests.engines.test_www1x import *  # noqa
26 37
 from searx.tests.engines.test_www500px import *  # noqa
38
+from searx.tests.engines.test_yacy import *  # noqa
39
+from searx.tests.engines.test_yahoo import *  # noqa
27 40
 from searx.tests.engines.test_youtube import *  # noqa
41
+from searx.tests.engines.test_yahoo_news import *  # noqa