aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/haketilo_test/unit/test_CORS_bypass_server.py65
-rw-r--r--test/haketilo_test/unit/test_content.py17
-rw-r--r--test/haketilo_test/unit/test_haketilo_apis.py129
-rw-r--r--test/haketilo_test/unit/test_install.py170
-rw-r--r--test/haketilo_test/unit/test_policy_deciding.py61
-rw-r--r--test/haketilo_test/unit/test_policy_enforcing.py10
-rw-r--r--test/haketilo_test/unit/test_popup.py22
-rw-r--r--test/haketilo_test/unit/test_repo_query.py64
-rw-r--r--test/haketilo_test/unit/test_repo_query_cacher.py19
-rw-r--r--test/haketilo_test/unit/test_webrequest.py4
-rw-r--r--test/haketilo_test/unit/utils.py73
-rw-r--r--test/haketilo_test/world_wide_library.py52
12 files changed, 456 insertions, 230 deletions
diff --git a/test/haketilo_test/unit/test_CORS_bypass_server.py b/test/haketilo_test/unit/test_CORS_bypass_server.py
index 45e4ebb..45f06a9 100644
--- a/test/haketilo_test/unit/test_CORS_bypass_server.py
+++ b/test/haketilo_test/unit/test_CORS_bypass_server.py
@@ -24,29 +24,28 @@ from selenium.webdriver.support.ui import WebDriverWait
from ..script_loader import load_script
from ..world_wide_library import some_data
-urls = {
- 'resource': 'https://anotherdoma.in/resource/blocked/by/CORS.json',
- 'nonexistent': 'https://nxdoma.in/resource.json',
- 'invalid': 'w3csucks://invalid.url/'
+datas = {
+ 'resource': 'https://anotherdoma.in/resource/blocked/by/CORS.json',
+ 'nonexistent': 'https://nxdoma.in/resource.json',
+ 'invalid': 'w3csucks://invalid.url/',
+ 'redirected_ok': 'https://site.with.scripts.block.ed',
+ 'redirected_err': 'https://site.with.scripts.block.ed'
}
+for name, url in [*datas.items()]:
+ datas[name] = {'url': url}
+
+datas['redirected_ok']['init'] = {'redirect': 'follow'}
+datas['redirected_err']['init'] = {'redirect': 'error'}
+
content_script = '''\
-const urls = %s;
-
-function fetch_data(url) {
- return {
- url,
- to_get: ["ok", "status"],
- to_call: ["text", "json"]
- };
-}
+const datas = %s;
async function fetch_resources() {
const results = {};
const promises = [];
- for (const [name, url] of Object.entries(urls)) {
- const sending = browser.runtime.sendMessage(["CORS_bypass",
- fetch_data(url)]);
+ for (const [name, data] of Object.entries(datas)) {
+ const sending = browser.runtime.sendMessage(["CORS_bypass", data]);
promises.push(sending.then(response => results[name] = response));
}
@@ -58,7 +57,7 @@ async function fetch_resources() {
fetch_resources();
'''
-content_script = content_script % json.dumps(urls);
+content_script = content_script % json.dumps(datas);
@pytest.mark.ext_data({
'content_script': content_script,
@@ -77,33 +76,41 @@ def test_CORS_bypass_server(driver, execute_in_page):
'''
const result = {};
let promises = [];
- for (const [name, url] of Object.entries(arguments[0])) {
+ for (const [name, data] of Object.entries(arguments[0])) {
const [ok_cb, err_cb] =
["ok", "err"].map(status => () => result[name] = status);
- promises.push(fetch(url).then(ok_cb, err_cb));
+ promises.push(fetch(data.url).then(ok_cb, err_cb));
}
// Make the promises non-failing.
promises = promises.map(p => new Promise(cb => p.then(cb, cb)));
returnval(Promise.all(promises).then(() => result));
''',
- {**urls, 'sameorigin': './nonexistent_resource'})
+ {**datas, 'sameorigin': './nonexistent_resource'})
- assert results == dict([*[(k, 'err') for k in urls.keys()],
+ assert results == dict([*[(k, 'err') for k in datas.keys()],
('sameorigin', 'ok')])
done = lambda d: d.execute_script('return window.haketilo_fetch_results;')
results = WebDriverWait(driver, 10).until(done)
assert set(results['invalid'].keys()) == {'error'}
+ assert results['invalid']['error']['fileName'].endswith('background.js')
+ assert type(results['invalid']['error']['lineNumber']) is int
+ assert type(results['invalid']['error']['message']) is str
+ assert results['invalid']['error']['name'] == 'TypeError'
- assert set(results['nonexistent'].keys()) == \
- {'ok', 'status', 'text', 'error_json'}
- assert results['nonexistent']['ok'] == False
assert results['nonexistent']['status'] == 404
- assert results['nonexistent']['text'] == 'Handler for this URL not found.'
+ assert results['nonexistent']['statusText'] == 'Not Found'
+ assert any([name.lower() == 'content-length'
+ for name, value in results['nonexistent']['headers']])
+ assert bytes.fromhex(results['nonexistent']['body']) == \
+ b'Handler for this URL not found.'
- assert set(results['resource'].keys()) == {'ok', 'status', 'text', 'json'}
- assert results['resource']['ok'] == True
assert results['resource']['status'] == 200
- assert results['resource']['text'] == some_data
- assert results['resource']['json'] == json.loads(some_data)
+ assert results['resource']['statusText'] == 'OK'
+ assert any([name.lower() == 'content-length'
+ for name, value in results['resource']['headers']])
+ assert bytes.fromhex(results['resource']['body']) == b'{"some": "data"}'
+
+ assert results['redirected_ok']['status'] == 200
+ assert results['redirected_err']['error']['name'] == 'TypeError'
diff --git a/test/haketilo_test/unit/test_content.py b/test/haketilo_test/unit/test_content.py
index 8220160..98ea930 100644
--- a/test/haketilo_test/unit/test_content.py
+++ b/test/haketilo_test/unit/test_content.py
@@ -88,6 +88,7 @@ content_script = \
}
repo_query_cacher.start = () => data_set("cacher_started", true);
+ haketilo_apis.start = () => data_set("apis_started", true);
enforce_blocking = policy => data_set("enforcing", policy);
@@ -118,7 +119,7 @@ content_script = \
@pytest.mark.ext_data({'content_script': content_script})
@pytest.mark.usefixtures('webextension')
-@pytest.mark.parametrize('target1', ['dynamic_before'])#, 'dynamic_after'])
+@pytest.mark.parametrize('target1', ['dynamic_before', 'dynamic_after'])
@pytest.mark.parametrize('target2', [
'scripts_blocked',
'payload_error',
@@ -144,6 +145,7 @@ def test_content_unprivileged_page(driver, execute_in_page, target1, target2):
assert data['bad_request_returned'] == False
assert data['cacher_started'] == True
+ assert data.get('apis_started', False) == (target2 == 'payload_ok')
for obj in (data['good_request_result'], data['enforcing']):
assert obj['allow'] == False
@@ -162,9 +164,13 @@ def test_content_unprivileged_page(driver, execute_in_page, target1, target2):
def vars_made_by_payload(driver):
vars_values = driver.execute_script(
- 'return [1, 2].map(n => window[`hak_injected_${n}`]);'
- )
- if vars_values != [None, None]:
+ '''
+ return [
+ ...[1, 2].map(n => window[`hak_injected_${n}`]),
+ window.haketilo_version
+ ];
+ ''')
+ if vars_values != [None, None, None]:
return vars_values
if target2 == 'payload_error':
@@ -174,7 +180,8 @@ def test_content_unprivileged_page(driver, execute_in_page, target1, target2):
}
elif target2 == 'payload_ok':
vars_values = WebDriverWait(driver, 10).until(vars_made_by_payload)
- assert vars_values == [1, 2]
+ assert vars_values[:2] == [1, 2]
+ assert type(vars_values[2]) == str
@pytest.mark.ext_data({'content_script': content_script})
@pytest.mark.usefixtures('webextension')
diff --git a/test/haketilo_test/unit/test_haketilo_apis.py b/test/haketilo_test/unit/test_haketilo_apis.py
new file mode 100644
index 0000000..af7906d
--- /dev/null
+++ b/test/haketilo_test/unit/test_haketilo_apis.py
@@ -0,0 +1,129 @@
+# SPDX-License-Identifier: CC0-1.0
+
+"""
+Haketilo unit tests - exposing some special functionalities to injected scripts
+"""
+
+# This file is part of Haketilo
+#
+# Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the CC0 1.0 Universal License as published by
+# the Creative Commons Corporation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# CC0 1.0 Universal License for more details.
+
+import pytest
+import json
+from selenium.webdriver.support.ui import WebDriverWait
+
+from ..script_loader import load_script
+from ..world_wide_library import some_data
+
+def content_script():
+ return load_script('content/haketilo_apis.js') + ';\nstart();'
+
+def background_script():
+ return load_script('background/CORS_bypass_server.js') + ';\nstart();'
+
+resource_url = 'https://anotherdoma.in/resource/blocked/by/CORS.json'
+
+@pytest.mark.ext_data({
+ 'content_script': content_script,
+ 'background_script': background_script
+})
+@pytest.mark.usefixtures('webextension')
+def test_haketilo_apis_CORS_bypass(driver):
+ """
+ Verify injected scripts will be able to bypass CORS with the help of
+ Haketilo API.
+ """
+ driver.get('https://gotmyowndoma.in/')
+
+ # First, verify that it is impossible to normally fetch the resource.
+ with pytest.raises(Exception, match='NetworkError'):
+ driver.execute_script('return fetch(arguments[0]);', resource_url)
+
+ # First, verify that it is possible to fetch the resource using API.
+ response = driver.execute_script(
+ '''
+ const fetch_arg = {
+ url: arguments[0],
+ init: {},
+ verify_that_nonstandard_properties_are_ignored: ":)"
+ };
+
+ const detail = {
+ data: JSON.stringify(fetch_arg),
+ id: "abcdef",
+ nonstandard_properties_verify_that_ignored_are: ":o"
+ };
+
+ let cb, done = new Promise(_cb => cb = _cb);
+ window.addEventListener("haketilo_CORS_bypass-abcdef",
+ e => cb(JSON.parse(e.detail)));
+ window.dispatchEvent(new CustomEvent("haketilo_CORS_bypass", {detail}));
+
+ return done;
+ ''',
+ resource_url)
+
+ assert response['body'] == some_data.encode().hex()
+ assert response['status'] == 200
+ assert type(response['headers']) is list
+
+@pytest.mark.ext_data({
+ 'content_script': content_script,
+ 'background_script': background_script
+})
+@pytest.mark.usefixtures('webextension')
+@pytest.mark.parametrize('error', [
+ 'bad url',
+ 'no_url',
+ 'non_string_url',
+ 'non_object_init',
+ 'non_object_detail',
+ 'non_string_id',
+ 'non_string_data'
+])
+def test_haketilo_apis_CORS_bypass_errors(driver, error):
+ """
+ Verify errors are returned properly by CORS_bypass API.
+ """
+ data = {
+ 'bad_url': {'url': 'muahahahaha', 'init': {}},
+ 'no_url': {'init': {}},
+ 'non_string_url': {'url': {}, 'init': {}},
+ 'non_object_init': {'url': {}, 'init': ":d"},
+ }.get(error, {'url': resource_url, 'init': {}})
+
+ detail = {
+ 'non_object_detail': '!!!',
+ 'non_string_id': {'data': json.dumps(data), 'id': None},
+ 'non_string_data': {'data': data, 'id': 'abcdef'}
+ }.get(error, {'data': json.dumps(data), 'id': 'abcdef'})
+
+ driver.get('https://gotmyowndoma.in/')
+
+ result = driver.execute_script(
+ '''
+ let cb, done = new Promise(_cb => cb = _cb);
+ window.addEventListener("haketilo_CORS_bypass-abcdef",
+ e => cb(JSON.parse(e.detail)));
+ window.dispatchEvent(new CustomEvent("haketilo_CORS_bypass",
+ {detail: arguments[0]}));
+ setTimeout(() => cb("timeout"), 5000);
+
+ return done;
+ ''',
+ detail)
+
+ if error in {'bad_url', 'no_url', 'non_string_url', 'non_object_init'}:
+ assert result['error']['name'] == 'TypeError'
+
+ if error in {'non_object_detail', 'non_string_id', 'non_string_data'}:
+ assert result == 'timeout'
diff --git a/test/haketilo_test/unit/test_install.py b/test/haketilo_test/unit/test_install.py
index 1e2063c..b1321ff 100644
--- a/test/haketilo_test/unit/test_install.py
+++ b/test/haketilo_test/unit/test_install.py
@@ -26,7 +26,7 @@ from ..script_loader import load_script
from .utils import *
def setup_view(driver, execute_in_page):
- mock_cacher(execute_in_page)
+ execute_in_page(mock_cacher_code)
execute_in_page(load_script('html/install.js'))
container_ids, containers_objects = execute_in_page(
@@ -57,8 +57,38 @@ install_ext_data = {
@pytest.mark.ext_data(install_ext_data)
@pytest.mark.usefixtures('webextension')
-@pytest.mark.parametrize('complex_variant', [False, True])
-def test_install_normal_usage(driver, execute_in_page, complex_variant):
+@pytest.mark.parametrize('variant', [{
+ # The resource/mapping others depend on.
+ 'root_resource_id': f'resource-abcd-defg-ghij',
+ 'root_mapping_id': f'mapping-abcd-defg-ghij',
+ # Those ids are used to check the alphabetical ordering.
+ 'item_ids': [f'resource-{letters}' for letters in (
+ 'a', 'abcd', 'abcd-defg-ghij', 'b', 'c',
+ 'd', 'defg', 'e', 'f',
+ 'g', 'ghij', 'h', 'i', 'j'
+ )],
+ 'files_count': 9
+}, {
+ 'root_resource_id': 'resource-a',
+ 'root_mapping_id': 'mapping-a',
+ 'item_ids': ['resource-a'],
+ 'files_count': 0
+}, {
+ 'root_resource_id': 'resource-a-w-required-mapping-v1',
+ 'root_mapping_id': 'mapping-a-w-required-mapping-v1',
+ 'item_ids': ['resource-a-w-required-mapping-v1'],
+ 'files_count': 1
+}, {
+ 'root_resource_id': 'resource-a-w-required-mapping-v2',
+ 'root_mapping_id': 'mapping-a-w-required-mapping-v2',
+ 'item_ids': [
+ 'mapping-a',
+ 'resource-a',
+ 'resource-a-w-required-mapping-v2'
+ ],
+ 'files_count': 1
+}])
+def test_install_normal_usage(driver, execute_in_page, variant):
"""
Test of the normal package installation procedure with one mapping and,
depending on parameter, one or many resources.
@@ -67,41 +97,27 @@ def test_install_normal_usage(driver, execute_in_page, complex_variant):
assert execute_in_page('returnval(shw());') == [[], False]
- if complex_variant:
- # The resource/mapping others depend on.
- root_id = 'abcd-defg-ghij'
- root_resource_id = f'resource-{root_id}'
- root_mapping_id = f'mapping-{root_id}'
- # Those ids are used to check the alphabetical ordering.
- resource_ids = [f'resource-{letters}' for letters in (
- 'a', 'abcd', root_id, 'b', 'c',
- 'd', 'defg', 'e', 'f',
- 'g', 'ghij', 'h', 'i', 'j'
- )]
- files_count = 9
- else:
- root_resource_id = f'resource-a'
- root_mapping_id = f'mapping-a'
- resource_ids = [root_resource_id]
- files_count = 0
-
# Preview the installation of a resource, show resource's details, close
# the details and cancel installation.
execute_in_page('returnval(install_view.show(...arguments));',
- 'https://hydril.la/', 'resource', root_resource_id)
+ 'https://hydril.la/', 'resource',
+ variant['root_resource_id'])
assert execute_in_page('returnval(shw());') == [['show'], True]
- assert f'{root_resource_id}-2021.11.11-1'\
+ assert f'{variant["root_resource_id"]}-2021.11.11-1'\
in containers['install_preview'].text
assert_container_displayed('install_preview')
entries = execute_in_page('returnval(ets().map(e => e.main_li.innerText));')
- assert len(entries) == len(resource_ids)
+ assert len(entries) == len(variant['item_ids'])
+ resource_idx = variant['item_ids'].index(variant['root_resource_id'])
# Verify alphabetical ordering.
- assert all([id in text for id, text in zip(resource_ids, entries)])
+ assert all([id in text for id, text in
+ zip(variant['item_ids'], entries)])
- assert not execute_in_page('returnval(ets()[0].old_ver);').is_displayed()
- execute_in_page('returnval(ets()[0].details_but);').click()
+ assert not execute_in_page(f'returnval(ets()[{resource_idx}].old_ver);')\
+ .is_displayed()
+ execute_in_page(f'returnval(ets()[{resource_idx}].details_but);').click()
assert 'resource-a' in containers['resource_preview_container'].text
assert_container_displayed('resource_preview_container')
@@ -116,20 +132,24 @@ def test_install_normal_usage(driver, execute_in_page, complex_variant):
# details, close the details and commit the installation.
execute_in_page('returnval(install_view.show(...arguments));',
'https://hydril.la/', 'mapping',
- root_mapping_id, [2022, 5, 10])
+ variant['root_mapping_id'], [2022, 5, 10])
assert execute_in_page('returnval(shw(2));') == [['show'], True]
assert_container_displayed('install_preview')
entries = execute_in_page('returnval(ets().map(e => e.main_li.innerText));')
- assert len(entries) == len(resource_ids) + 1
- assert f'{root_mapping_id}-2022.5.10' in entries[0]
+ assert len(entries) == len(variant['item_ids']) + 1
+
+ all_item_ids = sorted([*variant['item_ids'], variant['root_mapping_id']])
+ mapping_idx = all_item_ids.index(variant["root_mapping_id"])
# Verify alphabetical ordering.
- assert all([id in text for id, text in zip(resource_ids, entries[1:])])
+ assert all([id in text for id, text in zip(all_item_ids, entries)])
- assert not execute_in_page('returnval(ets()[0].old_ver);').is_displayed()
- execute_in_page('returnval(ets()[0].details_but);').click()
- assert root_mapping_id in containers['mapping_preview_container'].text
+ assert not execute_in_page(f'returnval(ets()[{mapping_idx}].old_ver);')\
+ .is_displayed()
+ execute_in_page(f'returnval(ets()[{mapping_idx}].details_but);').click()
+ assert variant['root_mapping_id'] in \
+ containers['mapping_preview_container'].text
assert_container_displayed('mapping_preview_container')
execute_in_page('returnval(install_view.mapping_back_but);').click()
@@ -145,16 +165,20 @@ def test_install_normal_usage(driver, execute_in_page, complex_variant):
# Verify the install
db_contents = get_db_contents(execute_in_page)
- for item_type, ids in \
- [('mapping', {root_mapping_id}), ('resource', set(resource_ids))]:
+ all_map_ids = {id for id in all_item_ids if id.startswith('mapping')}
+ all_res_ids = {id for id in all_item_ids if id.startswith('resource')}
+ for item_type, ids in [
+ ('mapping', all_map_ids),
+ ('resource', all_res_ids)
+ ]:
assert set([it['identifier'] for it in db_contents[item_type]]) == ids
- assert all([len(db_contents[store]) == files_count
+ assert all([len(db_contents[store]) == variant['files_count']
for store in ('file', 'file_uses')])
# Update the installed mapping to a newer version.
execute_in_page('returnval(install_view.show(...arguments));',
- 'https://hydril.la/', 'mapping', root_mapping_id)
+ 'https://hydril.la/', 'mapping', variant['root_mapping_id'])
assert execute_in_page('returnval(shw(4));') == [['show'], True]
# resources are already in the newest versions, hence they should not appear
# in the install preview list.
@@ -171,12 +195,19 @@ def test_install_normal_usage(driver, execute_in_page, complex_variant):
# Verify the newer version install.
old_db_contents, db_contents = db_contents, get_db_contents(execute_in_page)
- old_db_contents['mapping'][0]['version'][-1] += 1
- assert db_contents['mapping'] == old_db_contents['mapping']
+
+ old_root_mapping = [m for m in old_db_contents['mapping']
+ if m['identifier'] == variant['root_mapping_id']][0]
+ old_root_mapping['version'][-1] += 1
+
+ new_root_mapping = [m for m in db_contents['mapping']
+ if m['identifier'] == variant['root_mapping_id']][0]
+
+ assert old_root_mapping == new_root_mapping
# All items are up to date - verify dialog is instead shown in this case.
execute_in_page('install_view.show(...arguments);',
- 'https://hydril.la/', 'mapping', root_mapping_id)
+ 'https://hydril.la/', 'mapping', variant['root_mapping_id'])
fetched = lambda d: 'Fetching ' not in containers['dialog_container'].text
WebDriverWait(driver, 10).until(fetched)
@@ -203,7 +234,6 @@ def test_install_normal_usage(driver, execute_in_page, complex_variant):
'indexeddb_error_file_uses',
'failure_to_communicate_fetch',
'HTTP_code_file',
- 'not_valid_text',
'sha256_mismatch',
'indexeddb_error_write'
])
@@ -243,7 +273,7 @@ def test_install_dialogs(driver, execute_in_page, message):
if message == 'fetching_data':
execute_in_page(
'''
- browser.tabs.sendMessage = () => new Promise(cb => {});
+ window.mock_cacher_fetch = () => new Promise(cb => {});
install_view.show(...arguments);
''',
'https://hydril.la/', 'mapping', 'mapping-a')
@@ -253,7 +283,8 @@ def test_install_dialogs(driver, execute_in_page, message):
elif message == 'failure_to_communicate_sendmessage':
execute_in_page(
'''
- browser.tabs.sendMessage = () => Promise.resolve({error: "sth"});
+ window.mock_cacher_fetch =
+ () => {throw new Error("Something happened :o")};
install_view.show(...arguments);
''',
'https://hydril.la/', 'mapping', 'mapping-a')
@@ -262,8 +293,8 @@ def test_install_dialogs(driver, execute_in_page, message):
elif message == 'HTTP_code_item':
execute_in_page(
'''
- const response = {ok: false, status: 404};
- browser.tabs.sendMessage = () => Promise.resolve(response);
+ const response = new Response("", {status: 404});
+ window.mock_cacher_fetch = () => Promise.resolve(response);
install_view.show(...arguments);
''',
'https://hydril.la/', 'mapping', 'mapping-a')
@@ -272,8 +303,8 @@ def test_install_dialogs(driver, execute_in_page, message):
elif message == 'invalid_JSON':
execute_in_page(
'''
- const response = {ok: true, status: 200, error_json: "sth"};
- browser.tabs.sendMessage = () => Promise.resolve(response);
+ const response = new Response("sth", {status: 200});
+ window.mock_cacher_fetch = () => Promise.resolve(response);
install_view.show(...arguments);
''',
'https://hydril.la/', 'mapping', 'mapping-a')
@@ -282,12 +313,11 @@ def test_install_dialogs(driver, execute_in_page, message):
elif message == 'newer_API_version':
execute_in_page(
'''
- const old_sendMessage = browser.tabs.sendMessage;
- browser.tabs.sendMessage = async function(...args) {
- const response = await old_sendMessage(...args);
- response.json.$schema = "https://hydrilla.koszko.org/schemas/api_mapping_description-255.1.schema.json";
- return response;
- }
+ const newer_schema_url =
+ "https://hydrilla.koszko.org/schemas/api_mapping_description-255.1.schema.json";
+ const mocked_json_data = JSON.stringify({$schema: newer_schema_url});
+ const response = new Response(mocked_json_data, {status: 200});
+ window.mock_cacher_fetch = () => Promise.resolve(response);
install_view.show(...arguments);
''',
'https://hydril.la/', 'mapping', 'mapping-a', [2022, 5, 10])
@@ -297,12 +327,18 @@ def test_install_dialogs(driver, execute_in_page, message):
elif message == 'invalid_response_format':
execute_in_page(
'''
- const old_sendMessage = browser.tabs.sendMessage;
- browser.tabs.sendMessage = async function(...args) {
- const response = await old_sendMessage(...args);
- /* identifier is not a string as it should be. */
- response.json.identifier = 1234567;
- return response;
+ window.mock_cacher_fetch = async function(...args) {
+ const response = await fetch(...args);
+ const json = await response.json();
+
+ /* identifier is no longer a string as it should be. */
+ json.identifier = 1234567;
+
+ return new Response(JSON.stringify(json), {
+ status: response.status,
+ statusText: response.statusText,
+ headers: [...response.headers.entries()]
+ });
}
install_view.show(...arguments);
''',
@@ -352,7 +388,7 @@ def test_install_dialogs(driver, execute_in_page, message):
elif message == 'failure_to_communicate_fetch':
execute_in_page(
'''
- fetch = () => {throw "some error";};
+ fetch = () => {throw new Error("some error");};
returnval(install_view.show(...arguments));
''',
'https://hydril.la/', 'mapping', 'mapping-b')
@@ -372,18 +408,6 @@ def test_install_dialogs(driver, execute_in_page, message):
execute_in_page('returnval(install_view.install_but);').click()
assert_dlg(['conf_buts'], 'Repository sent HTTP code 400 :(')
- elif message == 'not_valid_text':
- execute_in_page(
- '''
- const err = () => {throw "some error";};
- fetch = () => Promise.resolve({ok: true, status: 200, text: err});
- returnval(install_view.show(...arguments));
- ''',
- 'https://hydril.la/', 'mapping', 'mapping-b')
-
- execute_in_page('returnval(install_view.install_but);').click()
-
- assert_dlg(['conf_buts'], "Repository's response is not valid text :(")
elif message == 'sha256_mismatch':
execute_in_page(
'''
diff --git a/test/haketilo_test/unit/test_policy_deciding.py b/test/haketilo_test/unit/test_policy_deciding.py
index 75b35ac..1be488f 100644
--- a/test/haketilo_test/unit/test_policy_deciding.py
+++ b/test/haketilo_test/unit/test_policy_deciding.py
@@ -23,19 +23,36 @@ import pytest
from ..script_loader import load_script
-csp_re = re.compile(r'^\S+\s+\S+;(?:\s+\S+\s+\S+;)*$')
-rule_re = re.compile(r'^\s*(?P<src_kind>\S+)\s+(?P<allowed_origins>\S+)$')
+csp_re = re.compile(r'''
+^
+\S+(?:\s+\S+)+; # first directive
+(?:
+ \s+\S+(?:\s+\S+)+; # subsequent directive
+)*
+$
+''',
+re.VERBOSE)
+
+rule_re = re.compile(r'''
+^
+\s*
+(?P<src_kind>\S+)
+\s+
+(?P<allowed_origins>
+ \S+(?:\s+\S+)*
+)
+$
+''', re.VERBOSE)
+
def parse_csp(csp):
- '''
- Parsing of CSP string into a dict. A simplified format of CSP is assumed.
- '''
+ '''Parsing of CSP string into a dict.'''
assert csp_re.match(csp)
result = {}
for rule in csp.split(';')[:-1]:
match = rule_re.match(rule)
- result[match.group('src_kind')] = match.group('allowed_origins')
+ result[match.group('src_kind')] = match.group('allowed_origins').split()
return result
@@ -78,10 +95,10 @@ def test_decide_policy(execute_in_page):
for prop in ('mapping', 'payload', 'nonce', 'error'):
assert prop not in policy
assert parse_csp(policy['csp']) == {
- 'prefetch-src': "'none'",
- 'script-src-attr': "'none'",
- 'script-src': "'none'",
- 'script-src-elem': "'none'"
+ 'prefetch-src': ["'none'"],
+ 'script-src-attr': ["'none'"],
+ 'script-src': ["'none'", "'unsafe-eval'"],
+ 'script-src-elem': ["'none'"]
}
policy = execute_in_page(
@@ -95,10 +112,10 @@ def test_decide_policy(execute_in_page):
for prop in ('payload', 'nonce', 'error'):
assert prop not in policy
assert parse_csp(policy['csp']) == {
- 'prefetch-src': "'none'",
- 'script-src-attr': "'none'",
- 'script-src': "'none'",
- 'script-src-elem': "'none'"
+ 'prefetch-src': ["'none'"],
+ 'script-src-attr': ["'none'"],
+ 'script-src': ["'none'", "'unsafe-eval'"],
+ 'script-src-elem': ["'none'"]
}
policy = execute_in_page(
@@ -114,10 +131,10 @@ def test_decide_policy(execute_in_page):
assert policy['nonce'] == \
sha256('m1:res1:http://kno.wn/:abcd'.encode()).digest().hex()
assert parse_csp(policy['csp']) == {
- 'prefetch-src': f"'none'",
- 'script-src-attr': f"'none'",
- 'script-src': f"'nonce-{policy['nonce']}'",
- 'script-src-elem': f"'nonce-{policy['nonce']}'"
+ 'prefetch-src': ["'none'"],
+ 'script-src-attr': ["'none'"],
+ 'script-src': [f"'nonce-{policy['nonce']}'", "'unsafe-eval'"],
+ 'script-src-elem': [f"'nonce-{policy['nonce']}'"]
}
policy = execute_in_page(
@@ -128,8 +145,8 @@ def test_decide_policy(execute_in_page):
for prop in ('mapping', 'payload', 'nonce'):
assert prop not in policy
assert parse_csp(policy['csp']) == {
- 'prefetch-src': "'none'",
- 'script-src-attr': "'none'",
- 'script-src': "'none'",
- 'script-src-elem': "'none'"
+ 'prefetch-src': ["'none'"],
+ 'script-src-attr': ["'none'"],
+ 'script-src': ["'none'", "'unsafe-eval'"],
+ 'script-src-elem': ["'none'"]
}
diff --git a/test/haketilo_test/unit/test_policy_enforcing.py b/test/haketilo_test/unit/test_policy_enforcing.py
index bbc3eb9..4bc6470 100644
--- a/test/haketilo_test/unit/test_policy_enforcing.py
+++ b/test/haketilo_test/unit/test_policy_enforcing.py
@@ -31,15 +31,19 @@ nonce = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
allow_policy = {'allow': True}
block_policy = {
'allow': False,
- 'csp': f"prefetch-src 'none'; script-src-attr 'none'; script-src 'none'; script-src-elem 'none'; frame-src http://* https://*;"
+ 'csp': f"prefetch-src 'none'; script-src-attr 'none'; script-src 'none' 'unsafe-eval'; script-src-elem 'none'; frame-src http://* https://*;"
}
payload_policy = {
'mapping': 'somemapping',
'payload': {'identifier': 'someresource'},
- 'csp': f"prefetch-src 'none'; script-src-attr 'none'; script-src 'nonce-{nonce}'; script-src-elem 'nonce-{nonce}';"
+ 'csp': f"prefetch-src 'none'; script-src-attr 'none'; script-src 'nonce-{nonce}' 'unsafe-eval'; script-src-elem 'nonce-{nonce}';"
}
-content_script = load_script('content/policy_enforcing.js') + ''';{
+def content_script():
+ return load_script('content/policy_enforcing.js') + \
+ content_script_appended_code
+
+content_script_appended_code = ''';{
const smuggled_what_to_do = /^[^#]*#?(.*)$/.exec(document.URL)[1];
const what_to_do = smuggled_what_to_do === "" ? {policy: {allow: true}} :
JSON.parse(decodeURIComponent(smuggled_what_to_do));
diff --git a/test/haketilo_test/unit/test_popup.py b/test/haketilo_test/unit/test_popup.py
index e62feb7..3ef7906 100644
--- a/test/haketilo_test/unit/test_popup.py
+++ b/test/haketilo_test/unit/test_popup.py
@@ -81,28 +81,18 @@ mocked_page_infos = {
tab_mock_js = '''
;
const mocked_page_info = (%s)[/#mock_page_info-(.*)$/.exec(document.URL)[1]];
+const old_sendMessage = browser.tabs.sendMessage;
browser.tabs.sendMessage = async function(tab_id, msg) {
const this_tab_id = (await browser.tabs.getCurrent()).id;
if (tab_id !== this_tab_id)
throw `not current tab id (${tab_id} instead of ${this_tab_id})`;
- if (msg[0] === "page_info") {
+ if (msg[0] === "page_info")
return mocked_page_info;
- } else if (msg[0] === "repo_query") {
- const response = await fetch(msg[1]);
- if (!response)
- return {error: "Something happened :o"};
-
- const result = {ok: response.ok, status: response.status};
- try {
- result.json = await response.json();
- } catch(e) {
- result.error_json = "" + e;
- }
- return result;
- } else {
+ else if (msg[0] === "repo_query")
+ return old_sendMessage(tab_id, msg);
+ else
throw `bad sendMessage message type: '${msg[0]}'`;
- }
}
const old_tabs_query = browser.tabs.query;
@@ -113,6 +103,8 @@ browser.tabs.query = async function(query) {
}
''' % json.dumps(mocked_page_infos)
+tab_mock_js = mock_cacher_code + tab_mock_js
+
popup_ext_data = {
'background_script': broker_js,
'extra_html': ExtraHTML(
diff --git a/test/haketilo_test/unit/test_repo_query.py b/test/haketilo_test/unit/test_repo_query.py
index f6cae93..c785406 100644
--- a/test/haketilo_test/unit/test_repo_query.py
+++ b/test/haketilo_test/unit/test_repo_query.py
@@ -29,7 +29,7 @@ repo_urls = [f'https://hydril.la/{s}' for s in ('', '1/', '2/', '3/', '4/')]
queried_url = 'https://example_a.com/something'
def setup_view(execute_in_page, repo_urls, tab={'id': 0}):
- mock_cacher(execute_in_page)
+ execute_in_page(mock_cacher_code)
execute_in_page(load_script('html/repo_query.js'))
execute_in_page(
@@ -185,8 +185,10 @@ def test_repo_query_messages(driver, execute_in_page, message):
elif message == 'failure_to_communicate':
setup_view(execute_in_page, repo_urls)
execute_in_page(
- 'browser.tabs.sendMessage = () => Promise.resolve({error: "sth"});'
- )
+ '''
+ window.mock_cacher_fetch =
+ () => {throw new Error("Something happened :o")};
+ ''')
show_and_wait_for_repo_entry()
elem = execute_in_page('returnval(view.repo_entries[0].info_div);')
@@ -196,8 +198,8 @@ def test_repo_query_messages(driver, execute_in_page, message):
setup_view(execute_in_page, repo_urls)
execute_in_page(
'''
- const response = {ok: false, status: 405};
- browser.tabs.sendMessage = () => Promise.resolve(response);
+ const response = new Response("", {status: 405});
+ window.mock_cacher_fetch = () => Promise.resolve(response);
''')
show_and_wait_for_repo_entry()
@@ -208,8 +210,8 @@ def test_repo_query_messages(driver, execute_in_page, message):
setup_view(execute_in_page, repo_urls)
execute_in_page(
'''
- const response = {ok: true, status: 200, error_json: "sth"};
- browser.tabs.sendMessage = () => Promise.resolve(response);
+ const response = new Response("sth", {status: 200});
+ window.mock_cacher_fetch = () => Promise.resolve(response);
''')
show_and_wait_for_repo_entry()
@@ -220,12 +222,11 @@ def test_repo_query_messages(driver, execute_in_page, message):
setup_view(execute_in_page, repo_urls)
execute_in_page(
'''
- const response = {
- ok: true,
- status: 200,
- json: {$schema: "https://hydrilla.koszko.org/schemas/api_query_result-255.2.1.schema.json"}
- };
- browser.tabs.sendMessage = () => Promise.resolve(response);
+ const newer_schema_url =
+ "https://hydrilla.koszko.org/schemas/api_query_result-255.2.1.schema.json";
+ const mocked_json_data = JSON.stringify({$schema: newer_schema_url});
+ const response = new Response(mocked_json_data, {status: 200});
+ window.mock_cacher_fetch = () => Promise.resolve(response);
''')
show_and_wait_for_repo_entry()
@@ -236,13 +237,19 @@ def test_repo_query_messages(driver, execute_in_page, message):
setup_view(execute_in_page, repo_urls)
execute_in_page(
'''
- const response = {
- ok: true,
- status: 200,
- /* $schema is not a string as it should be. */
- json: {$schema: null}
- };
- browser.tabs.sendMessage = () => Promise.resolve(response);
+ window.mock_cacher_fetch = async function(...args) {
+ const response = await fetch(...args);
+ const json = await response.json();
+
+ /* $schema is no longer a string as it should be. */
+ json.$schema = null;
+
+ return new Response(JSON.stringify(json), {
+ status: response.status,
+ statusText: response.statusText,
+ headers: [...response.headers.entries()]
+ });
+ }
''')
show_and_wait_for_repo_entry()
@@ -252,7 +259,7 @@ def test_repo_query_messages(driver, execute_in_page, message):
elif message == 'querying_repo':
setup_view(execute_in_page, repo_urls)
execute_in_page(
- 'browser.tabs.sendMessage = () => new Promise(() => {});'
+ 'window.mock_cacher_fetch = () => new Promise(cb => {});'
)
show_and_wait_for_repo_entry()
@@ -262,15 +269,12 @@ def test_repo_query_messages(driver, execute_in_page, message):
setup_view(execute_in_page, repo_urls)
execute_in_page(
'''
- const response = {
- ok: true,
- status: 200,
- json: {
- $schema: "https://hydrilla.koszko.org/schemas/api_query_result-1.schema.json",
- mappings: []
- }
- };
- browser.tabs.sendMessage = () => Promise.resolve(response);
+ const schema_url =
+ "https://hydrilla.koszko.org/schemas/api_query_result-1.schema.json";
+ const mocked_json_data =
+ JSON.stringify({$schema: schema_url, mappings: []});
+ const response = new Response(mocked_json_data, {status: 200});
+ window.mock_cacher_fetch = () => Promise.resolve(response);
''')
show_and_wait_for_repo_entry()
diff --git a/test/haketilo_test/unit/test_repo_query_cacher.py b/test/haketilo_test/unit/test_repo_query_cacher.py
index 5fbc5cd..3f0a00d 100644
--- a/test/haketilo_test/unit/test_repo_query_cacher.py
+++ b/test/haketilo_test/unit/test_repo_query_cacher.py
@@ -85,34 +85,35 @@ def run_content_script_in_new_window(driver, url):
'background_script': lambda: bypass_js() + ';' + tab_id_responder
})
@pytest.mark.usefixtures('webextension')
-def test_repo_query_cacher_normal_use(driver, execute_in_page):
+def test_repo_query_cacher_normal_use(driver):
"""
Test if HTTP requests made through our cacher return correct results.
"""
tab_id = run_content_script_in_new_window(driver, 'https://gotmyowndoma.in')
result = fetch_through_cache(driver, tab_id, 'https://counterdoma.in/')
- assert set(result.keys()) == {'ok', 'status', 'json'}
- counter_initial = result['json']['counter']
+ assert set(result.keys()) == {'status', 'statusText', 'headers', 'body'}
+ counter_initial = json.loads(bytes.fromhex(result['body']))['counter']
assert type(counter_initial) is int
for i in range(2):
result = fetch_through_cache(driver, tab_id, 'https://counterdoma.in/')
- assert result['json']['counter'] == counter_initial
+ assert json.loads(bytes.fromhex(result['body'])) \
+ == {'counter': counter_initial}
tab_id = run_content_script_in_new_window(driver, 'https://gotmyowndoma.in')
result = fetch_through_cache(driver, tab_id, 'https://counterdoma.in/')
- assert result['json']['counter'] == counter_initial + 1
+ assert json.loads(bytes.fromhex(result['body'])) \
+ == {'counter': counter_initial + 1}
for i in range(2):
result = fetch_through_cache(driver, tab_id, 'https://nxdoma.in/')
- assert set(result.keys()) == {'ok', 'status', 'error_json'}
- assert result['ok'] == False
assert result['status'] == 404
for i in range(2):
result = fetch_through_cache(driver, tab_id, 'bad://url')
assert set(result.keys()) == {'error'}
+ assert result['error']['name'] == 'TypeError'
@pytest.mark.ext_data({
'content_script': content_script,
@@ -128,3 +129,7 @@ def test_repo_query_cacher_bgscript_error(driver):
result = fetch_through_cache(driver, tab_id, 'https://counterdoma.in/')
assert set(result.keys()) == {'error'}
+ assert set(result['error'].keys()) == \
+ {'name', 'message', 'fileName', 'lineNumber'}
+ assert result['error']['message'] == \
+ "Couldn't communicate with background script."
diff --git a/test/haketilo_test/unit/test_webrequest.py b/test/haketilo_test/unit/test_webrequest.py
index 1244117..dc329b8 100644
--- a/test/haketilo_test/unit/test_webrequest.py
+++ b/test/haketilo_test/unit/test_webrequest.py
@@ -85,7 +85,7 @@ nonce = f'nonce-{sha256(nonce_source).digest().hex()}'
payload_csp_header = {
'name': f'Content-Security-Policy',
'value': ("prefetch-src 'none'; script-src-attr 'none'; "
- f"script-src '{nonce}'; script-src-elem '{nonce}';")
+ f"script-src '{nonce}' 'unsafe-eval'; script-src-elem '{nonce}';")
}
sample_payload_headers = [
@@ -107,7 +107,7 @@ sample_blocked_headers.append(sample_csp_header)
sample_blocked_headers.append({
'name': f'Content-Security-Policy',
'value': ("prefetch-src 'none'; script-src-attr 'none'; "
- f"script-src 'none'; script-src-elem 'none';")
+ "script-src 'none' 'unsafe-eval'; script-src-elem 'none';")
})
@pytest.mark.get_page('https://gotmyowndoma.in')
diff --git a/test/haketilo_test/unit/utils.py b/test/haketilo_test/unit/utils.py
index 7ddf92a..9b3e4a0 100644
--- a/test/haketilo_test/unit/utils.py
+++ b/test/haketilo_test/unit/utils.py
@@ -228,12 +228,21 @@ def are_scripts_allowed(driver, nonce=None):
return driver.execute_script(
'''
document.haketilo_scripts_allowed = false;
+ document.haketilo_eval_allowed = false;
const html_ns = "http://www.w3.org/1999/xhtml";
const script = document.createElementNS(html_ns, "script");
- script.innerHTML = "document.haketilo_scripts_allowed = true;";
+ script.innerHTML = `
+ document.haketilo_scripts_allowed = true;
+ eval('document.haketilo_eval_allowed = true;');
+ `;
if (arguments[0])
script.setAttribute("nonce", arguments[0]);
(document.head || document.documentElement).append(script);
+
+ if (document.haketilo_scripts_allowed !=
+ document.haketilo_eval_allowed)
+ throw "scripts allowed but eval blocked";
+
return document.haketilo_scripts_allowed;
''',
nonce)
@@ -246,36 +255,40 @@ def mock_broadcast(execute_in_page):
'Object.keys(broadcast).forEach(k => broadcast[k] = () => {});'
)
-def mock_cacher(execute_in_page):
- """
- Some parts of code depend on content/repo_query_cacher.js and
- background/CORS_bypass_server.js running in their appropriate contexts. This
- function modifies the relevant browser.runtime.sendMessage function to
- perform fetch(), bypassing the cacher.
- """
- execute_in_page(
- '''{
- const old_sendMessage = browser.tabs.sendMessage, old_fetch = fetch;
- async function new_sendMessage(tab_id, msg) {
- if (msg[0] !== "repo_query")
- return old_sendMessage(tab_id, msg);
-
- /* Use snapshotted fetch(), allow other test code to override it. */
- const response = await old_fetch(msg[1]);
- if (!response)
- return {error: "Something happened :o"};
-
- const result = {ok: response.ok, status: response.status};
- try {
- result.json = await response.json();
- } catch(e) {
- result.error_json = "" + e;
- }
- return result;
- }
+"""
+Some parts of code depend on content/repo_query_cacher.js and
+background/CORS_bypass_server.js running in their appropriate contexts. This
+snippet modifies the relevant browser.runtime.sendMessage function to perform
+fetch(), bypassing the cacher.
+"""
+mock_cacher_code = '''{
+const uint8_to_hex =
+ array => [...array].map(b => ("0" + b.toString(16)).slice(-2)).join("");
- browser.tabs.sendMessage = new_sendMessage;
- }''')
+const old_sendMessage = browser.tabs.sendMessage;
+window.mock_cacher_fetch = fetch;
+browser.tabs.sendMessage = async function(tab_id, msg) {
+ if (msg[0] !== "repo_query")
+ return old_sendMessage(tab_id, msg);
+
+ /*
+ * Use snapshotted fetch() under the name window.mock_cacher_fetch,
+ * allow other test code to override it.
+ */
+ try {
+ const response = await window.mock_cacher_fetch(msg[1]);
+ const buf = await response.arrayBuffer();
+ return {
+ status: response.status,
+ statusText: response.statusText,
+ headers: [...response.headers.entries()],
+ body: uint8_to_hex(new Uint8Array(buf))
+ }
+ } catch(e) {
+ return {error: {name: e.name, message: e.message}};
+ }
+}
+}'''
"""
Convenience snippet of code to retrieve a copy of given object with only those
diff --git a/test/haketilo_test/world_wide_library.py b/test/haketilo_test/world_wide_library.py
index 1a90c42..92ce97e 100644
--- a/test/haketilo_test/world_wide_library.py
+++ b/test/haketilo_test/world_wide_library.py
@@ -33,6 +33,8 @@ from shutil import rmtree
from threading import Lock
from uuid import uuid4
import json
+import functools as ft
+import operator as op
from .misc_constants import here
from .unit.utils import * # sample repo data
@@ -114,7 +116,7 @@ sample_contents = [f'Mi povas manĝi vitron, ĝi ne damaĝas min {i}'
for i in range(9)]
sample_hashes = [sha256(c.encode()).digest().hex() for c in sample_contents]
-file_url = lambda hashed: f'https://hydril.la/file/sha256/{hashed}'
+file_url = ft.partial(op.concat, 'https://hydril.la/file/sha256/')
sample_files_catalog = dict([(file_url(h), make_handler(c))
for h, c in zip(sample_hashes, sample_contents)])
@@ -144,18 +146,37 @@ for i in range(10):
'dependencies': []
})
+# The one below will generate items with schema still at version 1, so required
+# mappings will be ignored.
+sample_resource_templates.append({
+ 'id_suffix': 'a-w-required-mapping-v1',
+ 'files_count': 1,
+ 'dependencies': [],
+ 'required_mappings': [{'identifier': 'mapping-a'}],
+ 'include_in_query': False
+})
+
+sample_resource_templates.append({
+ 'id_suffix': 'a-w-required-mapping-v2',
+ 'files_count': 1,
+ 'dependencies': [],
+ 'required_mappings': [{'identifier': 'mapping-a'}],
+ 'schema_ver': '2',
+ 'include_in_query': False
+})
+
sample_resources_catalog = {}
sample_mappings_catalog = {}
sample_queries = {}
for srt in sample_resource_templates:
resource = make_sample_resource()
- resource['identifier'] = f'resource-{srt["id_suffix"]}'
- resource['long_name'] = resource['identifier'].upper()
- resource['uuid'] = str(uuid4())
- resource['dependencies'] = srt['dependencies']
- resource['source_copyright'] = []
- resource['scripts'] = []
+ resource['identifier'] = f'resource-{srt["id_suffix"]}'
+ resource['long_name'] = resource['identifier'].upper()
+ resource['uuid'] = str(uuid4())
+ resource['dependencies'] = srt['dependencies']
+ resource['source_copyright'] = []
+ resource['scripts'] = []
for i in range(srt['files_count']):
file_ref = {'file': f'file_{i}', 'sha256': sample_hashes[i]}
resource[('source_copyright', 'scripts')[i & 1]].append(file_ref)
@@ -174,22 +195,25 @@ for srt in sample_resource_templates:
sufs = [srt["id_suffix"], *[l for l in srt["id_suffix"] if l.isalpha()]]
patterns = [f'https://example_{suf}.com/*' for suf in set(sufs)]
- payloads = {}
+ mapping['payloads'] = {}
for pat in patterns:
- payloads[pat] = {'identifier': resource['identifier']}
+ mapping['payloads'][pat] = {'identifier': resource['identifier']}
- queryable_url = pat.replace('*', 'something')
- if queryable_url not in sample_queries:
- sample_queries[queryable_url] = []
+ if not srt.get('include_in_query', True):
+ continue
- sample_queries[queryable_url].append({
+ sample_queries.setdefault(pat.replace('*', 'something'), []).append({
'identifier': mapping['identifier'],
'long_name': mapping['long_name'],
'version': mapping_versions[1]
})
- mapping['payloads'] = payloads
+ for item in resource, mapping:
+ if 'required_mappings' in srt:
+ item['required_mappings'] = srt['required_mappings']
+ if 'schema_ver' in srt:
+ item['$schema'] = item['$schema'].replace('1', srt['schema_ver'])
for item, versions, catalog in [
(resource, resource_versions, sample_resources_catalog),