Skip to content
This repository has been archived by the owner on Jan 28, 2023. It is now read-only.

Add the test suite #2

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__
150 changes: 150 additions & 0 deletions test/firefox.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Copyright © 2014-2016 Jakub Wilk <jwilk@jwilk.net>
#
# 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 contextlib
import datetime
import os
import re
import shutil
import subprocess as ipc
import tempfile
import xml.etree.ElementTree as et

class Firefox(object):

def __init__(self, url):
self.url = url
self.child = None
version = ipc.check_output(['firefox', '--version'])
version = version.decode('ASCII')
version = re.search(r'(\d+)', version).group(0)
self.version = int(version)

def __enter__(self):
with open(os.devnull, 'wb') as dev_null:
self.child = ipc.Popen(
['firefox', '-no-remote', self.url],
stderr=dev_null,
)
return self

def __exit__(self, exc_type, exc_value, traceback):
if exc_type is not None:
self.child.terminate()
self.child.wait()

def wait(self):
self.child.wait()

def get_window_id(self, window_name):
cmdline = [
'xdotool',
'search',
'--sync',
'--limit', '1',
'--all',
'--pid', str(self.child.pid),
'--name', window_name,
]
output = ipc.check_output(cmdline)
return int(output)

def activate_window(self, window):
cmdline = ['xdotool']
if isinstance(window, int):
cmdline += [
'windowactivate', '--sync', str(window)
]
else:
cmdline += [
'search',
'--sync',
'--limit', '1',
'--all',
'--pid', str(self.child.pid),
'--name', window,
'windowactivate', '--sync',
]
ipc.check_call(cmdline)

def talk(self, window, *args):
self.activate_window(window)
cmdline = ['xdotool', 'key']
for arg in args:
if arg[:1] + arg[-1:] == '<>':
for ch in arg[1:-1]:
if ch == ' ':
cmdline += ['space']
else:
cmdline += [ch]
else:
cmdline += [arg]
ipc.check_call(cmdline)

profiles_ini = '''\
[General]
StartWithLastProfile=1

[Profile0]
Name=default
IsRelative=1
Path=default
'''

xml_namespaces = dict(
em='http://www.mozilla.org/2004/em-rdf#'
)

def get_addon_id():
rdf_path = os.path.dirname(__file__) + '/../src/install.rdf'
with open(rdf_path, 'rb') as rdf_file:
rdf_tree = et.parse(rdf_file)
id_elem = rdf_tree.find('.//em:id', namespaces=xml_namespaces)
return id_elem.text

prefs_js = '''
user_pref("network.proxy.http", "127.0.0.1");
user_pref("network.proxy.http_port", 9);
user_pref("network.proxy.ssl", "127.0.0.1");
user_pref("network.proxy.ssl_port", 9);
user_pref("network.proxy.type", 1);
user_pref("extensions.enabledItems", "{addon_id}:{today}");
'''.format(addon_id=get_addon_id(), today=datetime.date.today())

@contextlib.contextmanager
def clean_home_dir():
home = tempfile.mkdtemp(prefix='y-u-no-validate.')
try:
mozilla_home = home + '/.mozilla/firefox'
os.makedirs(mozilla_home + '/default')
with open(mozilla_home + '/profiles.ini', 'wt', encoding='ASCII') as file:
file.write(profiles_ini)
with open(mozilla_home + '/default/prefs.js', 'wt', encoding='ASCII') as file:
file.write(prefs_js)
old_home = os.environ['HOME']
os.environ['HOME'] = home
try:
yield
finally:
os.environ['HOME'] = old_home
finally:
shutil.rmtree(home)

# vim:ts=4 sts=4 sw=4 et
44 changes: 44 additions & 0 deletions test/httpd.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA8wYxnI1NiEhG3t/+b2XGBOiH0WUhLvG13aIrc/Ttm76uEa4/
Jpt0YtU+XNIN3gBSdzu7XYPsf2OZD8VhimrMDQHawECfOxEgyGfFEg3rqSFgJzwm
hiWf4+ZsTd0kFltohxkadpbNdSTKeU0/gCYUxmc1MsZf/S9NeWbZBcelj2+pfzFv
PwYfuuy7o+aOpCR/7CeOfTPStI0ngKPjM00Ozn9Lcpd/BVJ6hf+/K/3EontaYp1s
011z/h+pqKWUox9fOMsP90GQis3sB0D9rYffgN77f6Kc+8QcMYBjnHkD9/FXTKYP
hUjNVsqDD6sB5yIg/Che6T4RyStq31NzZghY5QIDAQABAoIBAFUUtgm47ovnwegF
Q258kvbk8ae9YACvXpxZSh0ugoEkIIzQFAvQIM75GPwmDfTK6BQWNWJn7UQr+kor
MBrliMqE+7CS7ywesvt2WOgZN4fex2r1BAey5GdFJGWeJiQtnEqv3zPlV8jLOJd/
E9wpeNx9BYLhMHnTNaxq9U+wtaWhI2fWa/0VqwY0Cf+N0BoOGRG4Zw1+slEg0gN7
AYws+r8hs0D446wATmMqtjrR6QUEBewXwvNpTNGidhHlDJwNeM62IGoSLN3FAnb6
SCFwYnB5GhfEp9e/KFdk1DNjhuazpyH+PPO/ngKYDuaR1Jbx5w2s46FPH5eRA/pp
0GT1+4ECgYEA/kScHaj+xwpUVsz7xrGTZBuQAQx3qI5I8M+BA3RmPL7GF/nS9yze
c6zk4adtW8NG12ml0RBdmiknXETDxnrUI25+OW1Ubvup4ALHuaoH3dy52SMOb+QO
D+bGUNT0T1TdCcLSt7WF3ROByqp6eSLG4tB7oS4G212MTDEHoVrIpV8CgYEA9K36
JAYfLYid7QLsv6XSl1qSonCrviVWvq1rp5wbOoo6mkU3zcV8TSWW1T5+KFkrG0AL
b10a+866zg4/AqFlx2/NNHBeajfSo3LWG1vF8ZCOs31w1UzVHBtzP9qIsmFxsaBC
ZS2zvpvxDKw7qBbCzscfhBbEPA72ScFsMNpDRDsCgYEA7k2DO1Edp5IIxWlMN0ZK
azJh3nm+09y2g+sWcRRwlVH++o2LqVkGC8foo4x1M+FqzY0YeT4rW8ZiO1m/Wo/X
rnVqG4xZ68I1zdsNMPcodEjgbZ8rcrZ8b27MQwmzB37zwqgzMTYRhcc4h6cLLejo
Gb9nfwJSLtoYGXiCPDmHf2UCgYBNM0+Haj8QGNjrXU7hsSpfAv7dLfuiRRm3k/Qx
sDmPIOoYntpanIL5vHB42/zmMiw9rtlsy82lwbaDKU+MPuxkHsx6TTIdBXv6glM3
0p8D9v5vd5bQViuvcKHOdd9HmSNMTipkziS2cXF+9CDmijfxEjbJcH1+DaJ75wGB
8Hvk3QKBgQC7KOnGiImY3TNJ3ZWPzSPEedZJhrEVukiwU1qhKTNTbnEShaS34mY3
GsQkvtbBXE9wqS44tQTLDSUxSdvWHGUUOgw92MBBRIN9tTOdqi2NiminaoWw3TYf
tOCE8N88Bao/H8ZQjV6nbXl0IbnalyGv8bh9yDOs+hGFYLL3GquW8Q==
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIICwjCCAaqgAwIBAgIEVY7wojANBgkqhkiG9w0BAQsFADAAMCIYDzIwMTUwNjI3
MTg1MTE0WhgPMjAxNjA2MjYxODUxMjFaMAAwggEiMA0GCSqGSIb3DQEBAQUAA4IB
DwAwggEKAoIBAQDzBjGcjU2ISEbe3/5vZcYE6IfRZSEu8bXdoitz9O2bvq4Rrj8m
m3Ri1T5c0g3eAFJ3O7tdg+x/Y5kPxWGKaswNAdrAQJ87ESDIZ8USDeupIWAnPCaG
JZ/j5mxN3SQWW2iHGRp2ls11JMp5TT+AJhTGZzUyxl/9L015ZtkFx6WPb6l/MW8/
Bh+67Luj5o6kJH/sJ459M9K0jSeAo+MzTQ7Of0tyl38FUnqF/78r/cSie1pinWzT
XXP+H6mopZSjH184yw/3QZCKzewHQP2th9+A3vt/opz7xBwxgGOceQP38VdMpg+F
SM1WyoMPqwHnIiD8KF7pPhHJK2rfU3NmCFjlAgMBAAGjQDA+MAwGA1UdEwEB/wQC
MAAwDwYDVR0PAQH/BAUDAwegADAdBgNVHQ4EFgQUbSMH+XYdHjNit33yjqLwq3Nv
K+MwDQYJKoZIhvcNAQELBQADggEBAI9Ao+e6zukr+uf0olKW0NSoueBvGL6NJIPD
6veuRSdQaxO8JXBWujdShGaAKZAhGnM8gqNF0sR2LPD0GM5Gi2jZJfMmV5g8Rjpb
g9jt8RkBbsVa7mtfuXJj7CM73LXrlIG5zGuGGqILr/6w+7+TVIAETqNcocRQks7w
KppAufUu4kj1snNe/K3a7TDiI2A7wjNHojOVfCHoE1v/a+4jw7APHGFzMPAEWpqi
RVp6hO9191hK5Ruts+6xAM3m8aRRHmJ1D4YQ/CaIt7NqFPAhmsdbRUJrICG5+08K
y33qjhhpgNC0OEsOIVc/s4Z4hSetdnVdyuLAhPulU55GDNUsVTQ=
-----END CERTIFICATE-----
65 changes: 65 additions & 0 deletions test/httpd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Copyright © 2014 Jakub Wilk <jwilk@jwilk.net>
#
# 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 http.server
import os
import ssl
import sys
import threading

page_event = threading.Event()

class RequestHandler(http.server.BaseHTTPRequestHandler):

def do_GET(self):
self.send_response(200)
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(b'Hello world!')
self.wfile.flush()
page_event.set()

def log_request(self, *args, **kwargs):
pass

class HTTPServer(http.server.HTTPServer):

def handle_error(self, request, client_address):
exc_type = sys.exc_info()[0]
if issubclass(exc_type, ssl.SSLError):
# SSL errors are expected and boring.
return
return super().handle_error(request, client_address)

def run():
httpd = HTTPServer(('127.0.0.1', 0), RequestHandler)
certfile = os.path.dirname(__file__) + '/httpd.pem'
httpd.socket = ssl.wrap_socket(
httpd.socket,
certfile=certfile,
server_side=True
)
httpd_thread = threading.Thread(target=httpd.serve_forever, daemon=True)
httpd_thread.start()
host, port = httpd.socket.getsockname()
url = 'https://{host}:{port}/'.format(host=host, port=port)
return url

# vim:ts=4 sts=4 sw=4 et
111 changes: 111 additions & 0 deletions test/run
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/usr/bin/python3

# Copyright © 2014-2016 Jakub Wilk <jwilk@jwilk.net>
#
# 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 contextlib
import os
import sys
import signal
import threading

import firefox
import httpd

class Timeout(RuntimeError):
default = 20

def raise_(*args, **kwargs):
raise Timeout

@contextlib.contextmanager
def check(message, *, timeout=Timeout.default):
if timeout is not None:
signal.signal(signal.SIGALRM, Timeout.raise_)
signal.alarm(timeout)
print(message, '...', end=' ')
sys.stdout.flush()
try:
yield
print('ok')
except Timeout:
print('TIMEOUT')
raise
except Exception:
print('ERROR')
raise
finally:
if timeout is not None:
signal.alarm(0)
signal.signal(signal.SIGALRM, signal.SIG_DFL)

def main():
with firefox.clean_home_dir():
_main()

def _main():
url = httpd.run()
with firefox.Firefox(url) as ff:
if ff.version >= 44:
untrusted_connection_title = 'Insecure Connection'
advanced_options_button = 'Advanced'
else:
untrusted_connection_title = 'Untrusted Connection'
advanced_options_button = 'I Understand The Risks'
with check('[1] untrusted connection'):
main_window_id = ff.get_window_id(untrusted_connection_title)
with check('[1] open "add security exception" dialog'):
ff.talk(main_window_id,
'ctrl+f', '<{0}>'.format(advanced_options_button.lower()),
'Escape', 'shift+Tab', 'space',
'ctrl+f', '<add exception>',
'Escape', 'shift+Tab', 'space',
)
with check('[1] add security exception'):
ff.talk('Add Security Exception',
'alt+c',
)
with check('[1] page loaded'):
httpd.page_event.wait()
with check('[1] quit Firefox'):
ff.talk(main_window_id, 'ctrl+q')
with check('[1] wait for Firefox termination'):
ff.wait()
httpd.page_event.clear()
with firefox.Firefox(url) as ff:
def alarm_on_page_event():
httpd.page_event.wait()
os.kill(os.getpid(), signal.SIGALRM)
thread = threading.Thread(target=alarm_on_page_event, daemon=True)
thread.start()
with check('[2] untrusted connection'):
try:
ff.talk(untrusted_connection_title, 'ctrl+q')
except Timeout:
if httpd.page_event.is_set():
raise RuntimeError('trusted connection')
raise
with check('[2] wait for Firefox termination'):
ff.wait()

if __name__ == '__main__':
main()

# vim:ts=4 sts=4 sw=4 et