# Copyright (c) 2017 crocoite contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
import pytest
from operator import itemgetter
from http.server import BaseHTTPRequestHandler
from pychrome.exceptions import TimeoutException
from .browser import Item, SiteLoader, ChromeService, NullService, BrowserCrashed
class TItem (Item):
""" This should be as close to Item as possible """
__slots__ = ('bodySend', '_body')
base = 'http://localhost:8000/'
def __init__ (self, path, status, headers, bodyReceive, bodySend=None):
super ().__init__ (tab=None)
self.chromeResponse = {'response': {'headers': headers, 'status': status, 'url': self.base + path}}
self._body = bodyReceive, False
self.bodySend = bodyReceive if not bodySend else bodySend
@property
def body (self):
return self._body
testItems = [
TItem ('binary', 200, {'Content-Type': 'application/octet-stream'}, b'\x00\x01\x02'),
TItem ('attachment', 200,
{'Content-Type': 'text/plain; charset=utf-8',
'Content-Disposition': 'attachment; filename="attachment.txt"',
},
'This is a simple text file with umlauts. ÄÖU.'.encode ('utf8')),
TItem ('encoding/utf8', 200, {'Content-Type': 'text/plain; charset=utf-8'},
'This is a test, äöü μνψκ ¥¥¥¿ýý¡'.encode ('utf8')),
TItem ('encoding/iso88591', 200, {'Content-Type': 'text/plain; charset=ISO-8859-1'},
'This is a test, äöü.'.encode ('utf8'),
'This is a test, äöü.'.encode ('ISO-8859-1')),
TItem ('encoding/latin1', 200, {'Content-Type': 'text/plain; charset=latin1'},
'This is a test, äöü.'.encode ('utf8'),
'This is a test, äöü.'.encode ('latin1')),
TItem ('image', 200, {'Content-Type': 'image/png'},
# 1×1 png image
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x00\x00\x00\x00:~\x9bU\x00\x00\x00\nIDAT\x08\x1dc\xf8\x0f\x00\x01\x01\x01\x006_g\x80\x00\x00\x00\x00IEND\xaeB`\x82'),
TItem ('empty', 200, {}, b''),
TItem ('redirect/301/empty', 301, {'Location': '/empty'}, b''),
TItem ('redirect/301/redirect/301/empty', 301, {'Location': '/redirect/301/empty'}, b''),
TItem ('nonexistent', 404, {}, b''),
TItem ('html', 200, {'Content-Type': 'html'},
'

'.encode ('utf8')),
TItem ('html/alert', 200, {'Content-Type': 'html'},
'
'.encode ('utf8')),
]
testItemMap = dict ([(item.parsedUrl.path, item) for item in testItems])
class RequestHandler (BaseHTTPRequestHandler):
def do_GET(self):
item = testItemMap.get (self.path)
if item:
self.send_response (item.response['status'])
for k, v in item.response['headers'].items ():
self.send_header (k, v)
body = item.bodySend
self.send_header ('Content-Length', len (body))
self.end_headers()
self.wfile.write (body)
return
def log_message (self, format, *args):
pass
@pytest.fixture
def http ():
def run ():
import http.server
PORT = 8000
httpd = http.server.HTTPServer (("localhost", PORT), RequestHandler)
print ('starting http server')
httpd.serve_forever()
from multiprocessing import Process
p = Process (target=run)
p.start ()
yield p
p.terminate ()
p.join ()
@pytest.fixture
def loader (http):
def f (path):
if path.startswith ('/'):
path = 'http://localhost:8000{}'.format (path)
return SiteLoader (browser, path)
print ('loader setup')
with ChromeService () as browser:
yield f
print ('loader teardown')
def itemsLoaded (l, items):
items = dict ([(i.parsedUrl.path, i) for i in items])
timeout = 5
while True:
if not l.notify.wait (timeout) and len (items) > 0:
assert False, 'timeout'
if len (l.queue) > 0:
item = l.queue.popleft ()
if isinstance (item, Exception):
raise item
assert item.chromeResponse is not None
golden = items.pop (item.parsedUrl.path)
if not golden:
assert False, 'url {} not supposed to be fetched'.format (item.url)
assert item.body[0] == golden.body[0]
assert item.response['status'] == golden.response['status']
assert item.statusText == BaseHTTPRequestHandler.responses.get (item.response['status'])[0]
for k, v in golden.responseHeaders:
actual = list (map (itemgetter (1), filter (lambda x: x[0] == k, item.responseHeaders)))
assert v in actual
# check queue at least once
if not items:
break
def literalItem (lf, item, deps=[]):
with lf (item.parsedUrl.path) as l:
l.start ()
itemsLoaded (l, [item] + deps)
def test_empty (loader):
literalItem (loader, testItemMap['/empty'])
def test_redirect (loader):
literalItem (loader, testItemMap['/redirect/301/empty'], [testItemMap['/empty']])
# chained redirects
literalItem (loader, testItemMap['/redirect/301/redirect/301/empty'], [testItemMap['/redirect/301/empty'], testItemMap['/empty']])
def test_encoding (loader):
""" Text responses are transformed to UTF-8. Make sure this works
correctly. """
for item in {testItemMap['/encoding/utf8'], testItemMap['/encoding/latin1'], testItemMap['/encoding/iso88591']}:
literalItem (loader, item)
def test_binary (loader):
""" Browser should ignore content it cannot display (i.e. octet-stream) """
with loader ('/binary') as l:
l.start ()
itemsLoaded (l, [])
def test_image (loader):
""" Images should be displayed inline """
literalItem (loader, testItemMap['/image'])
def test_attachment (loader):
""" And downloads won’t work in headless mode, even if it’s just a text file """
with loader ('/attachment') as l:
l.start ()
itemsLoaded (l, [])
def test_html (loader):
literalItem (loader, testItemMap['/html'], [testItemMap['/image'], testItemMap['/nonexistent']])
# make sure alerts are dismissed correctly (image won’t load otherwise)
literalItem (loader, testItemMap['/html/alert'], [testItemMap['/image']])
def test_crash (loader):
with loader ('/html') as l:
l.start ()
try:
l.tab.Page.crash (_timeout=1)
except TimeoutException:
pass
q = l.queue
assert isinstance (q.popleft (), BrowserCrashed)
def test_invalidurl (loader):
url = 'http://nonexistent.example/'
with loader (url) as l:
l.start ()
q = l.queue
it = q.popleft ()
assert it.failed
def test_nullservice ():
""" Null service returns the url as is """
url = 'http://localhost:12345'
with NullService (url) as u:
assert u == url