summaryrefslogtreecommitdiff
path: root/crocoite/test_browser.py
diff options
context:
space:
mode:
authorLars-Dominik Braun <lars@6xq.net>2018-06-20 21:01:33 +0200
committerLars-Dominik Braun <lars@6xq.net>2018-06-20 21:01:55 +0200
commitd77c58fc80a2fe62180796bdc28e8ed69bd46715 (patch)
tree71d1dc36ed680c19129de15daa2cb105a430784b /crocoite/test_browser.py
parent4302c6735d2985ed76e4a2d4a3319c7ef2c7ca84 (diff)
downloadcrocoite-d77c58fc80a2fe62180796bdc28e8ed69bd46715.tar.gz
crocoite-d77c58fc80a2fe62180796bdc28e8ed69bd46715.tar.bz2
crocoite-d77c58fc80a2fe62180796bdc28e8ed69bd46715.zip
Move tests to pytest
It just seems a little nicer than plain old unittest
Diffstat (limited to 'crocoite/test_browser.py')
-rw-r--r--crocoite/test_browser.py177
1 files changed, 177 insertions, 0 deletions
diff --git a/crocoite/test_browser.py b/crocoite/test_browser.py
new file mode 100644
index 0000000..cf7be39
--- /dev/null
+++ b/crocoite/test_browser.py
@@ -0,0 +1,177 @@
+# 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 .browser import Item, SiteLoader, ChromeService
+
+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'},
+ '<html><body><img src="/image"><img src="/nonexistent"></body></html>'.encode ('utf8')),
+ TItem ('html/alert', 200, {'Content-Type': 'html'},
+ '<html><body><script>window.addEventListener("beforeunload", function (e) { e.returnValue = "bye?"; return e.returnValue; }); alert("stopping here"); if (confirm("are you sure?") || prompt ("42?")) { window.location = "/nonexistent"; }</script><img src="/image"></body></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):
+ assert path.startswith ('/')
+ return SiteLoader (browser, 'http://localhost:8000{}'.format (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']
+ 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']])
+