summaryrefslogtreecommitdiff
path: root/test/unit
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2022-02-16 22:01:38 +0100
committerWojtek Kosior <koszko@koszko.org>2022-02-16 22:01:38 +0100
commitfd9f2fc4783cc606734e61116185c032a63d54a0 (patch)
treeddc162b1df608c3ae51d74f19fbffc92e5cfc3e3 /test/unit
parent7965f1b455144220c137bcb25c4967283a6b7ff3 (diff)
downloadbrowser-extension-fd9f2fc4783cc606734e61116185c032a63d54a0.tar.gz
browser-extension-fd9f2fc4783cc606734e61116185c032a63d54a0.zip
fix out-of-source builds
Diffstat (limited to 'test/unit')
-rw-r--r--test/unit/__init__.py2
-rw-r--r--test/unit/test_CORS_bypass_server.py109
-rw-r--r--test/unit/test_basic.py77
-rw-r--r--test/unit/test_broadcast.py175
-rw-r--r--test/unit/test_content.py190
-rw-r--r--test/unit/test_default_policy_dialog.py49
-rw-r--r--test/unit/test_dialog.py143
-rw-r--r--test/unit/test_indexeddb.py490
-rw-r--r--test/unit/test_indexeddb_files_server.py171
-rw-r--r--test/unit/test_install.py423
-rw-r--r--test/unit/test_item_list.py280
-rw-r--r--test/unit/test_item_preview.py208
-rw-r--r--test/unit/test_patterns.py152
-rw-r--r--test/unit/test_patterns_query_manager.py307
-rw-r--r--test/unit/test_patterns_query_tree.py474
-rw-r--r--test/unit/test_payload_create.py248
-rw-r--r--test/unit/test_policy_deciding.py135
-rw-r--r--test/unit/test_policy_enforcing.py114
-rw-r--r--test/unit/test_popup.py257
-rw-r--r--test/unit/test_repo_query.py274
-rw-r--r--test/unit/test_repo_query_cacher.py130
-rw-r--r--test/unit/test_settings.py63
-rw-r--r--test/unit/test_text_entry_list.py387
-rw-r--r--test/unit/test_webrequest.py68
-rw-r--r--test/unit/utils.py293
25 files changed, 0 insertions, 5219 deletions
diff --git a/test/unit/__init__.py b/test/unit/__init__.py
deleted file mode 100644
index 2b351bb..0000000
--- a/test/unit/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-# Copyright (C) 2021 Wojtek Kosior
diff --git a/test/unit/test_CORS_bypass_server.py b/test/unit/test_CORS_bypass_server.py
deleted file mode 100644
index 45e4ebb..0000000
--- a/test/unit/test_CORS_bypass_server.py
+++ /dev/null
@@ -1,109 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - routing HTTP requests through background script
-"""
-
-# 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
-
-urls = {
- 'resource': 'https://anotherdoma.in/resource/blocked/by/CORS.json',
- 'nonexistent': 'https://nxdoma.in/resource.json',
- 'invalid': 'w3csucks://invalid.url/'
-}
-
-content_script = '''\
-const urls = %s;
-
-function fetch_data(url) {
- return {
- url,
- to_get: ["ok", "status"],
- to_call: ["text", "json"]
- };
-}
-
-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)]);
- promises.push(sending.then(response => results[name] = response));
- }
-
- await Promise.all(promises);
-
- window.wrappedJSObject.haketilo_fetch_results = results;
-}
-
-fetch_resources();
-'''
-
-content_script = content_script % json.dumps(urls);
-
-@pytest.mark.ext_data({
- 'content_script': content_script,
- 'background_script':
- lambda: load_script('background/CORS_bypass_server.js') + '; start();'
-})
-@pytest.mark.usefixtures('webextension')
-def test_CORS_bypass_server(driver, execute_in_page):
- """
- Test if CORS bypassing works and if errors get properly forwarded.
- """
- driver.get('https://gotmyowndoma.in/')
-
- # First, verify that requests without CORS bypass measures fail.
- results = execute_in_page(
- '''
- const result = {};
- let promises = [];
- for (const [name, url] 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));
- }
- // 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'})
-
- assert results == dict([*[(k, 'err') for k in urls.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 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 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)
diff --git a/test/unit/test_basic.py b/test/unit/test_basic.py
deleted file mode 100644
index 6ec54cc..0000000
--- a/test/unit/test_basic.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - base
-"""
-
-# This file is part of Haketilo
-#
-# Copyright (C) 2021, Wojtek Kosior
-#
-# 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
-
-from ..script_loader import load_script
-from ..extension_crafting import ExtraHTML
-
-def test_driver(driver):
- """
- A trivial test case that verifies mocked web pages served by proxy can be
- accessed by the browser driven.
- """
- for proto in ['http://', 'https://']:
- driver.get(proto + 'gotmyowndoma.in')
- title = driver.execute_script(
- 'return document.getElementsByTagName("title")[0].innerText;'
- )
- assert "Schrodinger's Document" in title
-
-@pytest.mark.get_page('https://gotmyowndoma.in')
-def test_script_loader(execute_in_page):
- """
- A trivial test case that verifies Haketilo's .js files can be properly
- loaded into a test page together with their dependencies.
- """
- execute_in_page(load_script('common/indexeddb.js'))
-
- assert 'mapping' in execute_in_page('returnval(stores.map(s => s[0]));')
-
-@pytest.mark.ext_data({})
-@pytest.mark.usefixtures('webextension')
-def test_webextension(driver):
- """
- A trivial test case that verifies a test WebExtension created and installed
- by the `webextension` fixture works and redirects specially-constructed URLs
- to its test page.
- """
- heading = driver.execute_script(
- 'return document.getElementsByTagName("h1")[0].innerText;'
- )
- assert "Extension's options page for testing" in heading
-
-@pytest.mark.ext_data({
- 'extra_html': ExtraHTML(
- 'html/default_blocking_policy.html',
- {
- 'html/default_blocking_policy.js':
- 'document.body.innerHTML = `ski-ba-bop-ba ${typeof by_id}`;'
- }
- ),
- 'navigate_to': 'html/default_blocking_policy.html'
-})
-@pytest.mark.usefixtures('webextension')
-def test_extra_html(driver):
- """
- A trivial test case of the facility for loading the Haketilo's HTML files
- into test WebExtension for unit-testing.
- """
- assert driver.execute_script('return document.body.innerText') == \
- 'ski-ba-bop-ba function'
diff --git a/test/unit/test_broadcast.py b/test/unit/test_broadcast.py
deleted file mode 100644
index 7c2c051..0000000
--- a/test/unit/test_broadcast.py
+++ /dev/null
@@ -1,175 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - message broadcasting
-"""
-
-# This file is part of Haketilo
-#
-# Copyright (C) 2021, Wojtek Kosior
-#
-# 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
-from selenium.webdriver.support.ui import WebDriverWait
-
-from ..script_loader import load_script
-from .utils import broker_js
-
-test_page_html = '''
-<!DOCTYPE html>
-<script src="/testpage.js"></script>
-<h2>d0 (channel `somebodyoncetoldme`)</h2>
-<div id="d0"></div>
-<h2>d1 (channel `worldisgonnarollme`)</h2>
-<div id="d1"></div>
-<h2>d2 (both channels)</h2>
-<div id="d2"></div>
-'''
-
-@pytest.mark.ext_data({
- 'background_script': broker_js,
- 'test_page': test_page_html,
- 'extra_files': {
- 'testpage.js': lambda: load_script('common/broadcast.js')
- }
-})
-@pytest.mark.usefixtures('webextension')
-def test_broadcast(driver, execute_in_page, wait_elem_text):
- """
- A test that verifies the broadcasting system based on WebExtension messaging
- API and implemented in `background/broadcast_broker.js` and
- `common/broadcast.js` works correctly.
- """
- # The broadcast facility is meant to enable message distribution between
- # multiple contexts (e.g. different tabs/windows). Let's open the same
- # extension's test page in a second window.
- driver.execute_script(
- '''
- window.open(window.location.href, "_blank");
- window.open(window.location.href, "_blank");
- ''')
- WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) == 3)
- windows = [*driver.window_handles]
-
- # Let's first test if a simple message can be successfully broadcasted
- driver.switch_to.window(windows[0])
- execute_in_page(
- '''
- const divs = [0, 1, 2].map(n => document.getElementById("d" + n));
- let appender = n => (t => divs[n].append("\\n" + `[${t[0]}, ${t[1]}]`));
- let listener0 = listener_connection(appender(0));
- subscribe(listener0, "somebodyoncetoldme");
- ''')
-
- driver.switch_to.window(windows[1])
- execute_in_page(
- '''
- let sender0 = sender_connection();
- out(sender0, "somebodyoncetoldme", "iaintthesharpesttool");
- ''')
-
- driver.switch_to.window(windows[0])
- wait_elem_text('d0', '[somebodyoncetoldme, iaintthesharpesttool]')
-
- # Let's add 2 more listeners
- driver.switch_to.window(windows[0])
- execute_in_page(
- '''
- let listener1 = listener_connection(appender(1));
- subscribe(listener1, "worldisgonnarollme");
- let listener2 = listener_connection(appender(2));
- subscribe(listener2, "worldisgonnarollme");
- subscribe(listener2, "somebodyoncetoldme");
- ''')
-
- # Let's send one message to one channel and one to the other. Verify they
- # were received by the rght listeners.
- driver.switch_to.window(windows[1])
- execute_in_page(
- '''
- out(sender0, "somebodyoncetoldme", "intheshed");
- out(sender0, "worldisgonnarollme", "shewaslooking");
- ''')
-
- driver.switch_to.window(windows[0])
- wait_elem_text('d0', 'intheshed')
- wait_elem_text('d1', 'shewaslooking')
- wait_elem_text('d2', 'intheshed')
- wait_elem_text('d2', 'shewaslooking')
-
- text = execute_in_page('returnval(divs[0].innerText);')
- assert 'shewaslooking' not in text
- text = execute_in_page('returnval(divs[1].innerText);')
- assert 'intheshed' not in text
-
- # Let's create a second sender in third window and use it to send messages
- # with the 'prepare' feature.
- driver.switch_to.window(windows[2])
- execute_in_page(
- '''
- let sender1 = sender_connection();
- prepare(sender1, "somebodyoncetoldme", "kindadumb");
- out(sender1, "worldisgonnarollme", "withherfinger");
- ''')
-
- driver.switch_to.window(windows[0])
- wait_elem_text('d1', 'withherfinger')
- text = execute_in_page('returnval(divs[0].innerText);')
- assert 'kindadumb' not in text
-
- driver.switch_to.window(windows[2])
- execute_in_page('flush(sender1);')
-
- driver.switch_to.window(windows[0])
- wait_elem_text('d0', 'kindadumb')
-
- # Let's verify that prepare()'d messages are properly discarded when
- # discard() is called.
- driver.switch_to.window(windows[2])
- execute_in_page(
- '''
- prepare(sender1, "somebodyoncetoldme", "andherthumb");
- discard(sender1);
- prepare(sender1, "somebodyoncetoldme", "andhermiddlefinger");
- flush(sender1);
- ''')
-
- driver.switch_to.window(windows[0])
- wait_elem_text('d0', 'andhermiddlefinger')
- text = execute_in_page('returnval(divs[0].innerText);')
- assert 'andherthumb' not in text
-
- # Let's verify prepare()'d messages are properly auto-flushed when the other
- # end of the connection gets killed (e.g. because browser tab gets closed).
- driver.switch_to.window(windows[2])
- execute_in_page(
- '''
- prepare(sender1, "worldisgonnarollme", "intheshape", 500);
- ''')
- driver.close()
-
- driver.switch_to.window(windows[0])
- wait_elem_text('d2', 'intheshape')
-
- # Verify listener's connection gets closed properly.
- execute_in_page('close(listener0); close(listener1);')
-
- driver.switch_to.window(windows[1])
- execute_in_page('out(sender0, "worldisgonnarollme", "ofanL");')
- execute_in_page('out(sender0, "somebodyoncetoldme", "forehead");')
-
- driver.switch_to.window(windows[0])
- wait_elem_text('d2', 'ofanL')
- wait_elem_text('d2', 'forehead')
- for i in (0, 1):
- text = execute_in_page('returnval(divs[arguments[0]].innerText);', i)
- assert 'ofanL' not in text
- assert 'forehead' not in text
diff --git a/test/unit/test_content.py b/test/unit/test_content.py
deleted file mode 100644
index 8220160..0000000
--- a/test/unit/test_content.py
+++ /dev/null
@@ -1,190 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - main content script
-"""
-
-# 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:
-# https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/contentScripts/register
-# it is unclear whether the dynamically-registered content script is guaranteed
-# to be always executed after statically-registered ones. We want to test both
-# cases, so we'll make the mocked dynamic content script execute before
-# content.js on http:// pages and after it on https:// pages.
-dynamic_script = \
- ''';
- this.haketilo_secret = "abracadabra";
- this.haketilo_pattern_tree = {};
- this.haketilo_default_allow = false;
-
- if (this.haketilo_content_script_main)
- this.haketilo_content_script_main();
- '''
-
-content_script = \
- '''
- /* Mock dynamic content script - case 'before'. */
- if (/dynamic_before/.test(document.URL)) {
- %s;
- }
-
- /* Place amalgamated content.js here. */
- %s;
-
- /* Rest of mocks */
-
- function mock_decide_policy() {
- nonce = "12345";
- return {
- allow: false,
- mapping: "what-is-programmers-favorite-drinking-place",
- payload: {identifier: "foo-bar"},
- nonce,
- csp: "prefetch-src 'none'; script-src-attr 'none'; script-src 'nonce-12345'; script-src-elem 'nonce-12345';"
- };
- }
-
- async function mock_payload_error([type, res_id]) {
- if (type === "indexeddb_files")
- return {error: {haketilo_error_type: "missing", id: res_id}};
- }
-
- async function mock_payload_ok([type, res_id]) {
- if (type === "indexeddb_files")
- return {files: [1, 2].map(n => `window.hak_injected_${n} = ${n};`)};
- }
-
- if (/payload_error/.test(document.URL)) {
- browser.runtime.sendMessage = mock_payload_error;
- decide_policy = mock_decide_policy;
- } else if (/payload_ok/.test(document.URL)) {
- browser.runtime.sendMessage = mock_payload_ok;
- decide_policy = mock_decide_policy;
- }
- /* Otherwise, script blocking policy without payload to inject is used. */
-
- const data_to_verify = {};
- function data_set(prop, val) {
- data_to_verify[prop] = val;
- window.wrappedJSObject.data_to_verify = JSON.stringify(data_to_verify);
- }
-
- repo_query_cacher.start = () => data_set("cacher_started", true);
-
- enforce_blocking = policy => data_set("enforcing", policy);
-
- browser.runtime.onMessage.addListener = async function (listener_cb) {
- await new Promise(cb => setTimeout(cb, 10));
-
- /* Mock a good request. */
- const set_good = val => data_set("good_request_result", val);
- data_set("good_request_returned",
- !!listener_cb(["page_info"], {}, val => set_good(val)));
-
- /* Mock a bad request. */
- const set_bad = val => data_set("bad_request_result", val);
- data_set("bad_request_returned",
- !!listener_cb(["???"], {}, val => set_bad(val)));
- }
-
- /* main() call - normally present in content.js, inside '#IF !UNIT_TEST'. */
- main();
-
- /* Mock dynamic content script - case 'after'. */
- if (/#dynamic_after/.test(document.URL)) {
- %s;
- }
-
- data_set("script_run_without_errors", true);
- ''' % (dynamic_script, load_script('content/content.js'), dynamic_script)
-
-@pytest.mark.ext_data({'content_script': content_script})
-@pytest.mark.usefixtures('webextension')
-@pytest.mark.parametrize('target1', ['dynamic_before'])#, 'dynamic_after'])
-@pytest.mark.parametrize('target2', [
- 'scripts_blocked',
- 'payload_error',
- 'payload_ok'
-])
-def test_content_unprivileged_page(driver, execute_in_page, target1, target2):
- """
- Test functioning of content.js on an page using unprivileged schema (e.g.
- 'https://' and not 'about:').
- """
- driver.get(f'https://gotmyowndoma.in/index.html#{target1}-{target2}')
-
- def get_data(driver):
- data = driver.execute_script('return window.data_to_verify;')
- return data if 'good_request_result' in data else False
-
- data = json.loads(WebDriverWait(driver, 10).until(get_data))
-
- assert 'gotmyowndoma.in' in data['good_request_result']['url']
- assert 'bad_request_result' not in data
-
- assert data['good_request_returned'] == True
- assert data['bad_request_returned'] == False
-
- assert data['cacher_started'] == True
-
- for obj in (data['good_request_result'], data['enforcing']):
- assert obj['allow'] == False
-
- assert 'error' not in data['enforcing']
-
- if target2.startswith('payload'):
- for obj in (data['good_request_result'], data['enforcing']):
- assert obj['payload']['identifier'] == 'foo-bar'
- assert 'mapping' in obj
- else:
- assert 'payload' not in data['enforcing']
- assert 'mapping' not in data['enforcing']
-
- assert data['script_run_without_errors'] == True
-
- 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 vars_values
-
- if target2 == 'payload_error':
- assert data['good_request_result']['error'] == {
- 'haketilo_error_type': 'missing',
- 'id': 'foo-bar'
- }
- elif target2 == 'payload_ok':
- vars_values = WebDriverWait(driver, 10).until(vars_made_by_payload)
- assert vars_values == [1, 2]
-
-@pytest.mark.ext_data({'content_script': content_script})
-@pytest.mark.usefixtures('webextension')
-@pytest.mark.parametrize('target', ['dynamic_before', 'dynamic_after'])
-def test_content_privileged_page(driver, execute_in_page, target):
- """
- Test functioning of content.js on an page considered privileged (e.g. a
- directory listing at 'file:///').
- """
- driver.get(f'file:///#{target}')
- data = json.loads(driver.execute_script('return window.data_to_verify;'))
-
- assert data == {'script_run_without_errors': True}
diff --git a/test/unit/test_default_policy_dialog.py b/test/unit/test_default_policy_dialog.py
deleted file mode 100644
index a1c825f..0000000
--- a/test/unit/test_default_policy_dialog.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - default script blocking policy dialog
-"""
-
-# 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
-
-from ..extension_crafting import ExtraHTML
-from ..script_loader import load_script
-from .utils import broker_js
-
-@pytest.mark.ext_data({
- 'background_script': broker_js,
- 'extra_html': ExtraHTML(
- 'html/default_blocking_policy.html',
- {
- 'html/default_blocking_policy.js':
- 'init_default_policy_dialog();'
- }
- ),
- 'navigate_to': 'html/default_blocking_policy.html'
-})
-@pytest.mark.usefixtures('webextension')
-def test_default_blocking_policy_dialog(driver, wait_elem_text):
- """
- A test case for the dialog that facilitates toggling the default policy of
- script blocking.
- """
- wait_elem_text('current_policy_span', 'block')
-
- driver.find_element_by_id('toggle_policy_but').click()
- wait_elem_text('current_policy_span', 'allow')
-
- driver.find_element_by_id('toggle_policy_but').click()
- wait_elem_text('current_policy_span', 'block')
diff --git a/test/unit/test_dialog.py b/test/unit/test_dialog.py
deleted file mode 100644
index 63af79e..0000000
--- a/test/unit/test_dialog.py
+++ /dev/null
@@ -1,143 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - showing an error/info/question dalog
-"""
-
-# 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
-
-from ..extension_crafting import ExtraHTML
-from ..script_loader import load_script
-
-@pytest.mark.ext_data({
- 'extra_html': ExtraHTML('html/dialog.html', {}),
- 'navigate_to': 'html/dialog.html'
-})
-@pytest.mark.usefixtures('webextension')
-def test_dialog_show_close(driver, execute_in_page):
- """
- A test case of basic dialog showing/closing.
- """
- execute_in_page(load_script('html/dialog.js'))
- buts = execute_in_page(
- '''
- let cb_calls, call_prom;
- const dialog_context = make(() => cb_calls.push("show"),
- () => cb_calls.push("hide"));
- document.body.append(dialog_context.main_div);
- const buts = {};
- for (const but of document.getElementsByTagName("button"))
- buts[but.textContent] = but;
- returnval(buts);
- ''')
-
- for i, (dialog_function, but_text, hidden, expected_result) in enumerate([
- ('info', 'Ok', ['Yes', 'No'], None),
- ('error', 'Ok', ['Yes', 'No'], None),
- ('error', None, ['Yes', 'No'], None),
- ('loader', None, ['Yes', 'No', 'Ok'], None),
- ('ask', 'Yes', ['Ok'], True),
- ('ask', None, ['Ok'], None),
- ('ask', 'No', ['Ok'], False)
- ]):
- cb_calls, is_shown = execute_in_page(
- f'''
- cb_calls = [];
- call_prom = {dialog_function}(dialog_context,
- `sample_text_${{arguments[0]}}`);
- returnval([cb_calls, dialog_context.shown]);
- ''',
- i)
- assert cb_calls == ['show']
- assert is_shown == True
-
- page_source = driver.page_source
- assert f'sample_text_{i}' in page_source
- assert f'sample_text_{i - 1}' not in page_source
-
- # Verify the right buttons are displayed.
- for text, but in buts.items():
- if text in hidden:
- assert not but.is_displayed()
- # Verify clicking a hidden button does nothing.
- execute_in_page('buts[arguments[0]].click();', text)
- assert execute_in_page('returnval(cb_calls);') == cb_calls
- else:
- assert but.is_displayed()
-
- if but_text is None:
- execute_in_page('close_dialog(dialog_context);')
- else:
- buts[but_text].click()
-
- cb_calls, result, is_shown = execute_in_page(
- '''{
- const values_cb = r => [cb_calls, r, dialog_context.shown];
- returnval(call_prom.then(values_cb));
- }''')
- assert cb_calls == ['show', 'hide']
- assert result == expected_result
- assert is_shown == False
-
-@pytest.mark.ext_data({
- 'extra_html': ExtraHTML('html/dialog.html', {}),
- 'navigate_to': 'html/dialog.html'
-})
-@pytest.mark.usefixtures('webextension')
-def test_dialog_queue(driver, execute_in_page):
- """
- A test case of queuing dialog display operations.
- """
- execute_in_page(load_script('html/dialog.js'))
- execute_in_page(
- '''
- let cb_calls = [], call_proms = [];
- const dialog_context = make(() => cb_calls.push("show"),
- () => cb_calls.push("hide"));
- document.body.append(dialog_context.main_div);
- ''')
-
- buts = driver.find_elements_by_tag_name('button')
- buts = dict([(but.text, but) for but in buts])
-
- for i in range(5):
- cb_calls, is_shown, msg_elem = execute_in_page(
- '''
- call_proms.push(ask(dialog_context, "somequestion" + arguments[0]));
- returnval([cb_calls, dialog_context.shown, dialog_context.msg]);
- ''',
- i)
- assert cb_calls == ['show']
- assert is_shown == True
- assert msg_elem.text == 'somequestion0'
-
- for i in range(5):
- buts['Yes' if i & 1 else 'No'].click()
- cb_calls, is_shown, msg_elem, result = execute_in_page(
- '''{
- const values_cb =
- r => [cb_calls, dialog_context.shown, dialog_context.msg, r];
- returnval(call_proms.splice(0, 1)[0].then(values_cb));
- }''')
- if i < 4:
- assert cb_calls == ['show']
- assert is_shown == True
- assert msg_elem.text == f'somequestion{i + 1}'
- else:
- assert cb_calls == ['show', 'hide']
- assert is_shown == False
-
- assert result == bool(i & 1)
diff --git a/test/unit/test_indexeddb.py b/test/unit/test_indexeddb.py
deleted file mode 100644
index c2d5427..0000000
--- a/test/unit/test_indexeddb.py
+++ /dev/null
@@ -1,490 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - IndexedDB access
-"""
-
-# This file is part of Haketilo
-#
-# Copyright (C) 2021, 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.common.by import By
-from selenium.webdriver.support.ui import WebDriverWait
-from selenium.webdriver.support import expected_conditions as EC
-from selenium.common.exceptions import WebDriverException
-
-from ..script_loader import load_script
-from .utils import *
-
-# Sample resource definitions. They'd normally contain more fields but here we
-# use simplified versions.
-
-def make_sample_resource():
- return {
- 'source_copyright': [
- sample_file_ref('report.spdx'),
- sample_file_ref('LICENSES/somelicense.txt')
- ],
- 'type': 'resource',
- 'identifier': 'helloapple',
- 'scripts': [sample_file_ref('hello.js'), sample_file_ref('bye.js')]
- }
-
-def make_sample_mapping():
- return {
- 'source_copyright': [
- sample_file_ref('report.spdx'),
- sample_file_ref('README.md')
- ],
- 'type': 'mapping',
- 'identifier': 'helloapple'
- }
-
-@pytest.mark.get_page('https://gotmyowndoma.in')
-def test_haketilodb_item_modifications(driver, execute_in_page):
- """
- indexeddb.js facilitates operating on Haketilo's internal database.
- Verify database operations on mappings/resources work properly.
- """
- execute_in_page(load_script('common/indexeddb.js'))
- mock_broadcast(execute_in_page)
-
- # Start with no database.
- clear_indexeddb(execute_in_page)
-
- sample_item = make_sample_resource()
- sample_item['source_copyright'][0]['extra_prop'] = True
-
- execute_in_page(
- '''{
- const promise = start_items_transaction(["resource"], arguments[1])
- .then(ctx => save_item(arguments[0], ctx).then(() => ctx))
- .then(finalize_transaction);
- returnval(promise);
- }''',
- sample_item, {'sha256': sample_files_by_sha256})
-
- database_contents = get_db_contents(execute_in_page)
-
- assert len(database_contents['file']) == 4
- assert all([sample_files_by_sha256[file['sha256']] == file['contents']
- for file in database_contents['file']])
- assert all([len(file) == 2 for file in database_contents['file']])
-
- assert len(database_contents['file_uses']) == 4
- assert all([uses['uses'] == 1 for uses in database_contents['file_uses']])
- assert set([uses['sha256'] for uses in database_contents['file_uses']]) \
- == set([file['sha256'] for file in database_contents['file']])
-
- assert database_contents['mapping'] == []
- assert database_contents['resource'] == [sample_item]
-
- # See if trying to add an item without providing all its files ends in an
- # exception and aborts the transaction as it should.
- sample_item['scripts'].append(sample_file_ref('combined.js'))
- incomplete_files = {**sample_files_by_sha256}
- incomplete_files.pop(sample_files['combined.js']['sha256'])
- exception = execute_in_page(
- '''{
- const args = arguments;
- async function try_add_item()
- {
- const context =
- await start_items_transaction(["resource"], args[1]);
- try {
- await save_item(args[0], context);
- await finalize_transaction(context);
- return;
- } catch(e) {
- return e;
- }
- }
- returnval(try_add_item());
- }''',
- sample_item, {'sha256': incomplete_files})
-
- previous_database_contents = database_contents
- database_contents = get_db_contents(execute_in_page)
-
- assert 'file not present' in exception
- for key, val in database_contents.items():
- keyfun = lambda item: item.get('sha256') or item['identifier']
- assert sorted(previous_database_contents[key], key=keyfun) \
- == sorted(val, key=keyfun)
-
- # See if adding another item that partially uses first's files works OK.
- sample_item = make_sample_mapping()
- database_contents = execute_in_page(
- '''{
- const promise = start_items_transaction(["mapping"], arguments[1])
- .then(ctx => save_item(arguments[0], ctx).then(() => ctx))
- .then(finalize_transaction);
- returnval(promise);
- }''',
- sample_item, {'sha256': sample_files_by_sha256})
-
- database_contents = get_db_contents(execute_in_page)
-
- names = ['README.md', 'report.spdx', 'LICENSES/somelicense.txt', 'hello.js',
- 'bye.js']
- sample_files_list = [sample_files[name] for name in names]
- uses_list = [1, 2, 1, 1, 1]
-
- uses = dict([(uses['sha256'], uses['uses'])
- for uses in database_contents['file_uses']])
- assert uses == dict([(file['sha256'], nr)
- for file, nr in zip(sample_files_list, uses_list)])
-
- files = dict([(file['sha256'], file['contents'])
- for file in database_contents['file']])
- assert files == dict([(file['sha256'], file['contents'])
- for file in sample_files_list])
-
- del database_contents['resource'][0]['source_copyright'][0]['extra_prop']
- assert database_contents['resource'] == [make_sample_resource()]
- assert database_contents['mapping'] == [sample_item]
-
- # Try removing the items to get an empty database again.
- results = [None, None]
- for i, item_type in enumerate(['resource', 'mapping']):
- execute_in_page(
- f'''{{
- const remover = remove_{item_type};
- const promise =
- start_items_transaction(["{item_type}"], {{}})
- .then(ctx => remover('helloapple', ctx).then(() => ctx))
- .then(finalize_transaction);
- returnval(promise);
- }}''')
-
- results[i] = get_db_contents(execute_in_page)
-
- names = ['README.md', 'report.spdx']
- sample_files_list = [sample_files[name] for name in names]
- uses_list = [1, 1]
-
- uses = dict([(uses['sha256'], uses['uses'])
- for uses in results[0]['file_uses']])
- assert uses == dict([(file['sha256'], 1) for file in sample_files_list])
-
- files = dict([(file['sha256'], file['contents'])
- for file in results[0]['file']])
- assert files == dict([(file['sha256'], file['contents'])
- for file in sample_files_list])
-
- assert results[0]['resource'] == []
- assert results[0]['mapping'] == [sample_item]
-
- assert results[1] == dict([(key, []) for key in results[0].keys()])
-
- # Try initializing an empty database with sample initial data object.
- sample_resource = make_sample_resource()
- sample_mapping = make_sample_mapping()
- initial_data = {
- 'resource': {
- 'helloapple': {
- '1.12': sample_resource,
- '0.9': 'something_that_should_get_ignored',
- '1': 'something_that_should_get_ignored',
- '1.1': 'something_that_should_get_ignored',
- '1.11.1': 'something_that_should_get_ignored',
- }
- },
- 'mapping': {
- 'helloapple': {
- '0.1.1': sample_mapping
- }
- },
- 'file': {
- 'sha256': sample_files_by_sha256
- }
- }
-
- clear_indexeddb(execute_in_page)
- execute_in_page('initial_data = arguments[0];', initial_data)
- database_contents = get_db_contents(execute_in_page)
-
- assert database_contents['resource'] == [sample_resource]
- assert database_contents['mapping'] == [sample_mapping]
-
-@pytest.mark.get_page('https://gotmyowndoma.in')
-def test_haketilodb_settings(driver, execute_in_page):
- """
- indexeddb.js facilitates operating on Haketilo's internal database.
- Verify assigning/retrieving values of simple "setting" item works properly.
- """
- execute_in_page(load_script('common/indexeddb.js'))
- mock_broadcast(execute_in_page)
-
- # Start with no database.
- clear_indexeddb(execute_in_page)
-
- assert get_db_contents(execute_in_page)['setting'] == []
-
- assert execute_in_page('returnval(get_setting("option15"));') == None
-
- execute_in_page('returnval(set_setting("option15", "disable"));')
- assert execute_in_page('returnval(get_setting("option15"));') == 'disable'
-
- execute_in_page('returnval(set_setting("option15", "enable"));')
- assert execute_in_page('returnval(get_setting("option15"));') == 'enable'
-
-@pytest.mark.get_page('https://gotmyowndoma.in')
-def test_haketilodb_allowing(driver, execute_in_page):
- """
- indexeddb.js facilitates operating on Haketilo's internal database.
- Verify changing the "blocking" configuration for a URL works properly.
- """
- execute_in_page(load_script('common/indexeddb.js'))
- mock_broadcast(execute_in_page)
-
- # Start with no database.
- clear_indexeddb(execute_in_page)
-
- assert get_db_contents(execute_in_page)['blocking'] == []
-
- def run_with_sample_url(expr):
- return execute_in_page(f'returnval({expr});', 'https://example.com/**')
-
- assert None == run_with_sample_url('get_allowing(arguments[0])')
-
- run_with_sample_url('set_disallowed(arguments[0])')
- assert False == run_with_sample_url('get_allowing(arguments[0])')
-
- run_with_sample_url('set_allowed(arguments[0])')
- assert True == run_with_sample_url('get_allowing(arguments[0])')
-
- run_with_sample_url('set_default_allowing(arguments[0])')
- assert None == run_with_sample_url('get_allowing(arguments[0])')
-
-@pytest.mark.get_page('https://gotmyowndoma.in')
-def test_haketilodb_repos(driver, execute_in_page):
- """
- indexeddb.js facilitates operating on Haketilo's internal database.
- Verify operations on repositories list work properly.
- """
- execute_in_page(load_script('common/indexeddb.js'))
- mock_broadcast(execute_in_page)
-
- # Start with no database.
- clear_indexeddb(execute_in_page)
-
- assert get_db_contents(execute_in_page)['repo'] == []
-
- sample_urls = ['https://hdrlla.example.com/', 'https://hdrlla.example.org']
-
- assert [] == execute_in_page('returnval(get_repos());')
-
- execute_in_page('returnval(set_repo(arguments[0]));', sample_urls[0])
- assert [sample_urls[0]] == execute_in_page('returnval(get_repos());')
-
- execute_in_page('returnval(set_repo(arguments[0]));', sample_urls[1])
- assert set(sample_urls) == set(execute_in_page('returnval(get_repos());'))
-
- execute_in_page('returnval(del_repo(arguments[0]));', sample_urls[0])
- assert [sample_urls[1]] == execute_in_page('returnval(get_repos());')
-
-test_page_html = '''
-<!DOCTYPE html>
-<script src="/testpage.js"></script>
-<body>
-</body>
-'''
-
-@pytest.mark.ext_data({
- 'background_script': broker_js,
- 'test_page': test_page_html,
- 'extra_files': {
- 'testpage.js': lambda: load_script('common/indexeddb.js')
- }
-})
-@pytest.mark.usefixtures('webextension')
-def test_haketilodb_track(driver, execute_in_page, wait_elem_text):
- """
- Verify IndexedDB object change notifications are correctly broadcasted
- through extension's background script and allow for object store contents
- to be tracked in any execution context.
- """
- # Let's open the same extension's test page in a second window. Window 1
- # will be used to make changes to IndexedDB and window 0 to "track" those
- # changes.
- driver.execute_script('window.open(window.location.href, "_blank");')
- WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) == 2)
- windows = [*driver.window_handles]
-
- # Create elements that will have tracked data inserted under them.
- driver.switch_to.window(windows[0])
- execute_in_page(
- '''
- for (const store_name of trackable) {
- const h2 = document.createElement("h2");
- h2.innerText = store_name;
- document.body.append(h2);
-
- const ul = document.createElement("ul");
- ul.id = store_name;
- document.body.append(ul);
- }
- ''')
-
- # Mock initial_data.
- sample_resource = make_sample_resource()
- sample_mapping = make_sample_mapping()
- initial_data = {
- 'resource': {
- 'helloapple': {
- '1.0': sample_resource
- }
- },
- 'mapping': {
- 'helloapple': {
- '0.1.1': sample_mapping
- }
- },
- 'file': {
- 'sha256': sample_files_by_sha256
- }
- }
- driver.switch_to.window(windows[1])
- execute_in_page('initial_data = arguments[0];', initial_data)
- execute_in_page('returnval(set_setting("option15", "123"));')
- execute_in_page('returnval(set_repo("https://hydril.la"));')
- execute_in_page('returnval(set_disallowed("file:///*"));')
-
- # See if track.*() functions properly return the already-existing items.
- driver.switch_to.window(windows[0])
- execute_in_page(
- '''
- function update_item(store_name, change)
- {
- const elem_id = `${store_name}_${change.key}`;
- let elem = document.getElementById(elem_id);
- elem = elem || document.createElement("li");
- elem.id = elem_id;
- elem.innerText = JSON.stringify(change.new_val);
- document.getElementById(store_name).append(elem);
- if (change.new_val === undefined)
- elem.remove();
- }
-
- let resource_tracking, resource_items, mapping_tracking, mapping_items;
-
- async function start_reporting()
- {
- const props = new Map(stores.map(([sn, opt]) => [sn, opt.keyPath]));
- for (const store_name of trackable) {
- [tracking, items] =
- await track[store_name](ch => update_item(store_name, ch));
- const prop = props.get(store_name);
- for (const item of items)
- update_item(store_name, {key: item[prop], new_val: item});
- }
- }
-
- returnval(start_reporting());
- ''')
-
- item_counts = execute_in_page(
- '''{
- const childcount = id => document.getElementById(id).childElementCount;
- returnval(trackable.map(childcount));
- }''')
- assert item_counts == [1 for _ in item_counts]
- for elem_id, json_value in [
- ('resource_helloapple', sample_resource),
- ('mapping_helloapple', sample_mapping),
- ('setting_option15', {'name': 'option15', 'value': '123'}),
- ('repo_https://hydril.la', {'url': 'https://hydril.la'}),
- ('blocking_file:///*', {'pattern': 'file:///*', 'allow': False})
- ]:
- assert json.loads(driver.find_element_by_id(elem_id).text) == json_value
-
- # See if item additions get tracked properly.
- driver.switch_to.window(windows[1])
- sample_resource2 = make_sample_resource()
- sample_resource2['identifier'] = 'helloapple-copy'
- sample_mapping2 = make_sample_mapping()
- sample_mapping2['identifier'] = 'helloapple-copy'
- sample_data = {
- 'resource': {
- 'helloapple-copy': {
- '1.0': sample_resource2
- }
- },
- 'mapping': {
- 'helloapple-copy': {
- '0.1.1': sample_mapping2
- }
- },
- 'file': {
- 'sha256': sample_files_by_sha256
- },
- 'repo': [
- 'https://hydril2.la/'
- ]
- }
- execute_in_page('returnval(save_items(arguments[0]));', sample_data)
- execute_in_page('returnval(set_setting("option22", "abc"));')
- execute_in_page('returnval(set_repo("https://hydril3.la/"));')
- execute_in_page('returnval(set_allowed("ftp://a.bc/"));')
-
- driver.switch_to.window(windows[0])
- driver.implicitly_wait(10)
- for elem_id, json_value in [
- ('resource_helloapple-copy', sample_resource2),
- ('mapping_helloapple-copy', sample_mapping2),
- ('setting_option22', {'name': 'option22', 'value': 'abc'}),
- ('repo_https://hydril2.la/', {'url': 'https://hydril2.la/'}),
- ('repo_https://hydril3.la/', {'url': 'https://hydril3.la/'}),
- ('blocking_ftp://a.bc/', {'pattern': 'ftp://a.bc/', 'allow': True})
- ]:
- assert json.loads(driver.find_element_by_id(elem_id).text) == json_value
- driver.implicitly_wait(0)
-
- # See if item deletions/modifications get tracked properly.
- driver.switch_to.window(windows[1])
- execute_in_page(
- '''{
- async function change_remove_items()
- {
- const store_names = ["resource", "mapping"];
- const ctx = await start_items_transaction(store_names, {});
- await remove_resource("helloapple", ctx);
- await remove_mapping("helloapple-copy", ctx);
- await finalize_transaction(ctx);
- await set_setting("option22", null);
- await del_repo("https://hydril.la");
- await set_default_allowing("file:///*");
- await set_disallowed("ftp://a.bc/");
- }
- returnval(change_remove_items());
- }''')
-
- removed_ids = ['mapping_helloapple-copy', 'resource_helloapple',
- 'repo_https://hydril.la', 'blocking_file:///*']
- def condition_items_absent_and_changed(driver):
- for id in removed_ids:
- try:
- driver.find_element_by_id(id)
- return False
- except WebDriverException:
- pass
-
- option_text = driver.find_element_by_id('setting_option22').text
- blocking_text = driver.find_element_by_id('blocking_ftp://a.bc/').text
- return (json.loads(option_text)['value'] == None and
- json.loads(blocking_text)['allow'] == False)
-
- driver.switch_to.window(windows[0])
- WebDriverWait(driver, 10).until(condition_items_absent_and_changed)
diff --git a/test/unit/test_indexeddb_files_server.py b/test/unit/test_indexeddb_files_server.py
deleted file mode 100644
index 6ddfba8..0000000
--- a/test/unit/test_indexeddb_files_server.py
+++ /dev/null
@@ -1,171 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - serving indexeddb resource script files to content scripts
-"""
-
-# This file is part of Haketilo
-#
-# Copyright (C) 2021,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 copy
-from uuid import uuid4
-from selenium.webdriver.support.ui import WebDriverWait
-
-from ..script_loader import load_script
-from .utils import *
-
-"""
-How many test resources we're going to have.
-"""
-count = 15
-
-sample_files_list = [(f'file_{n}_{i}', f'contents {n} {i}')
- for n in range(count) for i in range(2)]
-
-sample_files = dict(sample_files_list)
-
-sample_files, sample_files_by_sha256 = make_sample_files(sample_files)
-
-def make_sample_resource_with_deps(n):
- resource = make_sample_resource(with_files=False)
-
- resource['identifier'] = f'res-{n}'
- resource['dependencies'] = [{'identifier': f'res-{m}'}
- for m in range(max(n - 4, 0), n)]
- resource['scripts'] = [sample_file_ref(f'file_{n}_{i}', sample_files)
- for i in range(2)]
-
- return resource
-
-resources = [make_sample_resource_with_deps(n) for n in range(count)]
-
-sample_data = {
- 'resource': sample_data_dict(resources),
- 'mapping': {},
- 'file': {
- 'sha256': sample_files_by_sha256
- }
-}
-
-def prepare_test_page(initial_indexeddb_data, execute_in_page):
- js = load_script('background/indexeddb_files_server.js',
- code_to_add='#IMPORT common/broadcast.js')
- execute_in_page(js)
-
- mock_broadcast(execute_in_page)
- clear_indexeddb(execute_in_page)
-
- execute_in_page(
- '''
- let registered_listener;
- const new_addListener = cb => registered_listener = cb;
-
- browser = {runtime: {onMessage: {addListener: new_addListener}}};
-
- haketilodb.save_items(arguments[0]);
-
- start();
- ''',
- initial_indexeddb_data)
-
-@pytest.mark.get_page('https://gotmyowndoma.in')
-def test_indexeddb_files_server_normal_usage(driver, execute_in_page):
- """
- Test querying resource files (with resource dependency resolution)
- from IndexedDB and serving them in messages to content scripts.
- """
- prepare_test_page(sample_data, execute_in_page)
-
- # Verify other types of messages are ignored.
- function_returned_value = execute_in_page(
- '''
- returnval(registered_listener(["???"], {},
- () => location.reload()));
- ''')
- assert function_returned_value == None
-
- # Verify single resource's files get properly resolved.
- function_returned_value = execute_in_page(
- '''
- var result_cb, contents_prom = new Promise(cb => result_cb = cb);
-
- returnval(registered_listener(["indexeddb_files", "res-0"],
- {}, result_cb));
- ''')
- assert function_returned_value == True
-
- assert execute_in_page('returnval(contents_prom);') == \
- {'files': [tuple[1] for tuple in sample_files_list[0:2]]}
-
- # Verify multiple resources' files get properly resolved.
- function_returned_value = execute_in_page(
- '''
- var result_cb, contents_prom = new Promise(cb => result_cb = cb);
-
- returnval(registered_listener(["indexeddb_files", arguments[0]],
- {}, result_cb));
- ''',
- f'res-{count - 1}')
- assert function_returned_value == True
-
- assert execute_in_page('returnval(contents_prom);') == \
- {'files': [tuple[1] for tuple in sample_files_list]}
-
-@pytest.mark.get_page('https://gotmyowndoma.in')
-@pytest.mark.parametrize('error', [
- 'missing',
- 'circular',
- 'db',
- 'other'
-])
-def test_indexeddb_files_server_errors(driver, execute_in_page, error):
- """
- Test reporting of errors when querying resource files (with resource
- dependency resolution) from IndexedDB and serving them in messages to
- content scripts.
- """
- sample_data_copy = copy.deepcopy(sample_data)
-
- if error == 'missing':
- del sample_data_copy['resource']['res-3']
- elif error == 'circular':
- res3_defs = sample_data_copy['resource']['res-3'].values()
- next(iter(res3_defs))['dependencies'].append({'identifier': 'res-8'})
-
- prepare_test_page(sample_data_copy, execute_in_page)
-
- if error == 'db':
- execute_in_page('haketilodb.idb_get = t => t.onerror("oooops");')
- elif error == 'other':
- execute_in_page('haketilodb.idb_get = () => {throw "oooops"};')
-
- response = execute_in_page(
- '''
- var result_cb, contents_prom = new Promise(cb => result_cb = cb);
-
- registered_listener(["indexeddb_files", arguments[0]],
- {}, result_cb);
-
- returnval(contents_prom);
- ''',
- f'res-{count - 1}')
-
- assert response['error']['haketilo_error_type'] == error
-
- if error == 'missing':
- assert response['error']['id'] == 'res-3'
- elif error == 'circular':
- assert response['error']['id'] in ('res-3', 'res-8')
- elif error not in ('db', 'other'):
- raise Exception('made a typo in test function params?')
diff --git a/test/unit/test_install.py b/test/unit/test_install.py
deleted file mode 100644
index f4bc483..0000000
--- a/test/unit/test_install.py
+++ /dev/null
@@ -1,423 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - item installation dialog
-"""
-
-# 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 ..extension_crafting import ExtraHTML
-from ..script_loader import load_script
-from .utils import *
-
-def setup_view(driver, execute_in_page):
- mock_cacher(execute_in_page)
-
- execute_in_page(load_script('html/install.js'))
- container_ids, containers_objects = execute_in_page(
- '''
- const cb_calls = [];
- const install_view = new InstallView(0,
- () => cb_calls.push("show"),
- () => cb_calls.push("hide"));
- document.body.append(install_view.main_div);
- const ets = () => install_view.item_entries;
- const shw = slice => [cb_calls.slice(slice || 0), install_view.shown];
- returnval([container_ids, container_ids.map(cid => install_view[cid])]);
- ''')
-
- containers = dict(zip(container_ids, containers_objects))
-
- def assert_container_displayed(container_id):
- for cid, cobj in zip(container_ids, containers_objects):
- assert (cid == container_id) == cobj.is_displayed()
-
- return containers, assert_container_displayed
-
-install_ext_data = {
- 'background_script': broker_js,
- 'extra_html': ExtraHTML('html/install.html', {}),
- 'navigate_to': 'html/install.html'
-}
-
-@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):
- """
- Test of the normal package installation procedure with one mapping and,
- depending on parameter, one or many resources.
- """
- containers, assert_container_displayed = setup_view(driver, execute_in_page)
-
- 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)
-
- assert execute_in_page('returnval(shw());') == [['show'], True]
- assert f'{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)
- # Verify alphabetical ordering.
- assert all([id in text for id, text in zip(resource_ids, entries)])
-
- assert not execute_in_page('returnval(ets()[0].old_ver);').is_displayed()
- execute_in_page('returnval(ets()[0].details_but);').click()
- assert 'resource_a' in containers['resource_preview_container'].text
- assert_container_displayed('resource_preview_container')
-
- execute_in_page('returnval(install_view.resource_back_but);').click()
- assert_container_displayed('install_preview')
-
- assert execute_in_page('returnval(shw());') == [['show'], True]
- execute_in_page('returnval(install_view.cancel_but);').click()
- assert execute_in_page('returnval(shw());') == [['show', 'hide'], False]
-
- # Preview the installation of a mapping and a resource, show mapping's
- # 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])
-
- 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]
- # Verify alphabetical ordering.
- assert all([id in text for id, text in zip(resource_ids, entries[1:])])
-
- 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_container_displayed('mapping_preview_container')
-
- execute_in_page('returnval(install_view.mapping_back_but);').click()
- assert_container_displayed('install_preview')
-
- execute_in_page('returnval(install_view.install_but);').click()
- installed = lambda d: 'ly installed!' in containers['dialog_container'].text
- WebDriverWait(driver, 10).until(installed)
-
- assert execute_in_page('returnval(shw(2));') == [['show'], True]
- execute_in_page('returnval(install_view.dialog_ctx.ok_but);').click()
- assert execute_in_page('returnval(shw(2));') == [['show', 'hide'], False]
-
- # Verify the install
- db_contents = get_db_contents(execute_in_page)
- for item_type, ids in \
- [('mapping', {root_mapping_id}), ('resource', set(resource_ids))]:
- assert set([it['identifier'] for it in db_contents[item_type]]) == ids
-
- assert all([len(db_contents[store]) == 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)
- 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.
- assert execute_in_page('returnval(ets().length);') == 1
- # Mapping's version update information should be displayed.
- assert execute_in_page('returnval(ets()[0].old_ver);').is_displayed()
- execute_in_page('returnval(install_view.install_but);').click()
-
- WebDriverWait(driver, 10).until(installed)
-
- assert execute_in_page('returnval(shw(4));') == [['show'], True]
- execute_in_page('returnval(install_view.dialog_ctx.ok_but);').click()
- assert execute_in_page('returnval(shw(4));') == [['show', 'hide'], False]
-
- # 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']
-
- # 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)
-
- fetched = lambda d: 'Fetching ' not in containers['dialog_container'].text
- WebDriverWait(driver, 10).until(fetched)
-
- assert 'Nothing to do - packages already installed.' \
- in containers['dialog_container'].text
- assert_container_displayed('dialog_container')
-
- assert execute_in_page('returnval(shw(6));') == [['show'], True]
- execute_in_page('returnval(install_view.dialog_ctx.ok_but);').click()
- assert execute_in_page('returnval(shw(6));') == [['show', 'hide'], False]
-
-@pytest.mark.ext_data(install_ext_data)
-@pytest.mark.usefixtures('webextension')
-@pytest.mark.parametrize('message', [
- 'fetching_data',
- 'failure_to_communicate_sendmessage',
- 'HTTP_code_item',
- 'invalid_JSON',
- 'newer_API_version',
- 'invalid_response_format',
- 'indexeddb_error_item',
- 'installing',
- 'indexeddb_error_file_uses',
- 'failure_to_communicate_fetch',
- 'HTTP_code_file',
- 'not_valid_text',
- 'sha256_mismatch',
- 'indexeddb_error_write'
-])
-def test_install_dialogs(driver, execute_in_page, message):
- """
- Test of various error and loading messages used in install view.
- """
- containers, assert_container_displayed = setup_view(driver, execute_in_page)
-
- def dlg_buts():
- return execute_in_page(
- '''{
- const dlg = install_view.dialog_ctx;
- const ids = ['ask_buts', 'conf_buts'];
- returnval(ids.filter(id => !dlg[id].classList.contains("hide")));
- }''')
-
- def dialog_txt():
- return execute_in_page(
- 'returnval(install_view.dialog_ctx.msg.textContent);'
- )
-
- def assert_dlg(awaited_buttons, expected_msg, hides_install_view=True,
- button_to_click='ok_but'):
- WebDriverWait(driver, 10).until(lambda d: dlg_buts() == awaited_buttons)
-
- assert expected_msg == dialog_txt()
-
- execute_in_page(
- f'returnval(install_view.dialog_ctx.{button_to_click});'
- ).click()
-
- if hides_install_view:
- assert execute_in_page('returnval(shw());') == \
- [['show', 'hide'], False]
-
- if message == 'fetching_data':
- execute_in_page(
- '''
- browser.tabs.sendMessage = () => new Promise(cb => {});
- install_view.show(...arguments);
- ''',
- 'https://hydril.la/', 'mapping', 'mapping_a')
-
- assert dlg_buts() == []
- assert dialog_txt() == 'Fetching data from repository...'
- elif message == 'failure_to_communicate_sendmessage':
- execute_in_page(
- '''
- browser.tabs.sendMessage = () => Promise.resolve({error: "sth"});
- install_view.show(...arguments);
- ''',
- 'https://hydril.la/', 'mapping', 'mapping_a')
-
- assert_dlg(['conf_buts'], 'Failure to communicate with repository :(')
- elif message == 'HTTP_code_item':
- execute_in_page(
- '''
- const response = {ok: false, status: 404};
- browser.tabs.sendMessage = () => Promise.resolve(response);
- install_view.show(...arguments);
- ''',
- 'https://hydril.la/', 'mapping', 'mapping_a')
-
- assert_dlg(['conf_buts'], 'Repository sent HTTP code 404 :(')
- elif message == 'invalid_JSON':
- execute_in_page(
- '''
- const response = {ok: true, status: 200, error_json: "sth"};
- browser.tabs.sendMessage = () => Promise.resolve(response);
- install_view.show(...arguments);
- ''',
- 'https://hydril.la/', 'mapping', 'mapping_a')
-
- assert_dlg(['conf_buts'], "Repository's response is not valid JSON :(")
- elif message == 'newer_API_version':
- execute_in_page(
- '''
- const response = {
- ok: true,
- status: 200,
- json: {$schema: "https://hydrilla.koszko.org/schemas/api_mapping_description-2.1.schema.json"}
- };
- browser.tabs.sendMessage = () => Promise.resolve(response);
- install_view.show(...arguments);
- ''',
- 'https://hydril.la/', 'mapping', 'somemapping', [2, 1])
-
- assert_dlg(['conf_buts'],
- 'Mapping somemapping-2.1 was served using unsupported Hydrilla API version. You might need to update Haketilo.')
- elif message == 'invalid_response_format':
- 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);
- install_view.show(...arguments);
- ''',
- 'https://hydril.la/', 'resource', 'someresource')
-
- assert_dlg(['conf_buts'],
- 'Resource someresource was served using a nonconforming response format.')
- elif message == 'indexeddb_error_item':
- execute_in_page(
- '''
- haketilodb.idb_get = () => {throw "some error";};
- install_view.show(...arguments);
- ''',
- 'https://hydril.la/', 'mapping', 'mapping_a')
-
- assert_dlg(['conf_buts'],
- "Error accessing Haketilo's internal database :(")
- elif message == 'installing':
- execute_in_page(
- '''
- haketilodb.save_items = () => new Promise(() => {});
- returnval(install_view.show(...arguments));
- ''',
- 'https://hydril.la/', 'mapping', 'mapping_b')
-
- execute_in_page('returnval(install_view.install_but);').click()
-
- assert dlg_buts() == []
- assert dialog_txt() == 'Installing...'
- elif message == 'indexeddb_error_file_uses':
- execute_in_page(
- '''
- const old_idb_get = haketilodb.idb_get;
- haketilodb.idb_get = function(transaction, store_name, identifier) {
- if (store_name === "file_uses")
- throw "some error";
- return old_idb_get(...arguments);
- }
- returnval(install_view.show(...arguments));
- ''',
- 'https://hydril.la/', 'mapping', 'mapping_b')
-
- execute_in_page('returnval(install_view.install_but);').click()
-
- assert_dlg(['conf_buts'],
- "Error accessing Haketilo's internal database :(")
- elif message == 'failure_to_communicate_fetch':
- execute_in_page(
- '''
- fetch = () => {throw "some error";};
- returnval(install_view.show(...arguments));
- ''',
- 'https://hydril.la/', 'mapping', 'mapping_b')
-
- execute_in_page('returnval(install_view.install_but);').click()
-
- assert_dlg(['conf_buts'],
- 'Failure to communicate with repository :(')
- elif message == 'HTTP_code_file':
- execute_in_page(
- '''
- fetch = () => Promise.resolve({ok: false, status: 400});
- 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 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(
- '''
- let old_fetch = fetch, url_used;
- fetch = async function(url) {
- url_used = url;
- const response = await old_fetch(...arguments);
- const text = () => response.text().then(t => t + ":d");
- return {ok: response.ok, status: response.status, text};
- }
- returnval(install_view.show(...arguments));
- ''',
- 'https://hydril.la/', 'mapping', 'mapping_b')
-
- execute_in_page('returnval(install_view.install_but);').click()
-
- get_url_used = lambda d: execute_in_page('returnval(url_used);')
- url_used = WebDriverWait(driver, 10).until(get_url_used)
- print ((url_used,))
-
- assert dlg_buts() == ['conf_buts']
- assert dialog_txt() == \
- f'{url_used} served a file with different SHA256 cryptographic sum :('
- elif message == 'indexeddb_error_write':
- execute_in_page(
- '''
- haketilodb.save_items = () => {throw "some error";};
- returnval(install_view.show(...arguments));
- ''',
- 'https://hydril.la/', 'mapping', 'mapping_b')
-
- execute_in_page('returnval(install_view.install_but);').click()
-
- assert_dlg(['conf_buts'],
- "Error writing to Haketilo's internal database :(")
- else:
- raise Exception('made a typo in test function params?')
diff --git a/test/unit/test_item_list.py b/test/unit/test_item_list.py
deleted file mode 100644
index 35ed1d5..0000000
--- a/test/unit/test_item_list.py
+++ /dev/null
@@ -1,280 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - displaying list of resources/mappings
-"""
-
-# 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
-from selenium.webdriver.support.ui import WebDriverWait
-
-from ..extension_crafting import ExtraHTML
-from ..script_loader import load_script
-from .utils import *
-
-def make_sample_resource(identifier, long_name):
- return {
- 'source_name': 'hello',
- 'source_copyright': [
- sample_file_ref('report.spdx'),
- sample_file_ref('LICENSES/CC0-1.0.txt')
- ],
- 'type': 'resource',
- 'identifier': identifier,
- 'long_name': long_name,
- 'uuid': 'a6754dcb-58d8-4b7a-a245-24fd7ad4cd68',
- 'version': [2021, 11, 10],
- 'revision': 1,
- 'description': 'greets an apple',
- 'dependencies': [{'identifier': 'hello-message'}],
- 'scripts': [
- sample_file_ref('hello.js'),
- sample_file_ref('bye.js')
- ]
- }
-
-def make_sample_mapping(identifier, long_name):
- return {
- 'source_name': 'example-org-fixes-new',
- 'source_copyright': [
- sample_file_ref('report.spdx'),
- sample_file_ref('LICENSES/CC0-1.0.txt')
- ],
- 'type': 'mapping',
- 'identifier': identifier,
- 'long_name': long_name,
- 'uuid': '54d23bba-472e-42f5-9194-eaa24c0e3ee7',
- 'version': [2022, 5, 10],
- 'description': 'suckless something something',
- 'payloads': {
- 'https://example.org/a/*': {
- 'identifier': 'some-KISS-resource'
- },
- 'https://example.org/t/*': {
- 'identifier': 'another-KISS-resource'
- }
- }
- }
-
-def make_item(item_type, *args):
- return make_sample_resource(*args) if item_type == 'resource' \
- else make_sample_mapping(*args)
-
-@pytest.mark.ext_data({
- 'background_script': broker_js,
- 'extra_html': ExtraHTML('html/item_list.html', {}),
- 'navigate_to': 'html/item_list.html'
-})
-@pytest.mark.usefixtures('webextension')
-@pytest.mark.parametrize('item_type', ['resource', 'mapping'])
-def test_item_list_ordering(driver, execute_in_page, item_type):
- """
- A test case of items list proper ordering.
- """
- execute_in_page(load_script('html/item_list.js'))
-
- # Choose sample long names so as to test automatic sorting of items.
- long_names = ['sample', 'sample it', 'Sample it', 'SAMPLE IT',
- 'test', 'test it', 'Test it', 'TEST IT']
- # Let's operate on a reverse-sorted copy
- long_names_reversed = [*long_names]
- long_names_reversed.reverse()
-
- items = [make_item(item_type, f'it_{hex(2 * i + copy)[-1]}', name)
- for i, name in enumerate(long_names_reversed)
- for copy in (1, 0)]
- # When adding/updating items this item will be updated at the end and this
- # last update will be used to verify that a set of opertions completed.
- extra_item = make_item(item_type, 'extraitem', 'extra item')
-
- # After this reversal items are sorted in the exact order they are expected
- # to appear in the HTML list.
- items.reverse()
-
- sample_data = {
- 'resource': {},
- 'mapping': {},
- 'file': {
- 'sha256': sample_files_by_sha256
- }
- }
-
- indexes_added = set()
- for iteration, to_include in enumerate([
- set([i for i in range(len(items)) if is_prime(i)]),
- set([i for i in range(len(items))
- if not is_prime(i) and i & 1]),
- set([i for i in range(len(items)) if i % 3 == 0]),
- set([i for i in range(len(items))
- if i % 3 and not i & 1 and not is_prime(i)]),
- set(range(len(items)))
- ]):
- # On the last iteration, re-add ALL items but with changed names.
- if len(to_include) == len(items):
- for it in items:
- it['long_name'] = f'somewhat renamed {it["long_name"]}'
-
- items_to_inclue = [items[i] for i in sorted(to_include)]
- sample_data[item_type] = sample_data_dict(items_to_inclue)
- execute_in_page('returnval(haketilodb.save_items(arguments[0]));',
- sample_data)
-
- extra_item['long_name'] = f'{iteration} {extra_item["long_name"]}'
- sample_data[item_type] = sample_data_dict([extra_item])
- execute_in_page('returnval(haketilodb.save_items(arguments[0]));',
- sample_data)
-
- if iteration == 0:
- execute_in_page(
- f'''
- let list_ctx;
- async function create_list() {{
- list_ctx = await {item_type}_list();
- document.body.append(list_ctx.main_div);
- }}
- returnval(create_list());
- ''')
-
- def lis_ready(driver):
- return extra_item['long_name'] == execute_in_page(
- 'returnval(list_ctx.ul.firstElementChild.textContent);'
- )
-
- indexes_added.update(to_include)
- WebDriverWait(driver, 10).until(lis_ready)
-
- li_texts = execute_in_page(
- '''
- var lis = [...list_ctx.ul.children].slice(1);
- returnval(lis.map(li => li.textContent));
- ''')
- assert li_texts == [items[i]['long_name'] for i in indexes_added]
-
- preview_texts = execute_in_page(
- '''{
- const get_texts =
- li => [li.click(), list_ctx.preview_container.textContent][1];
- returnval(lis.map(get_texts));
- }''')
-
- for i, text in zip(sorted(indexes_added), preview_texts):
- assert items[i]['identifier'] in text
- assert items[i]['long_name'] in text
-
-@pytest.mark.ext_data({
- 'background_script': broker_js,
- 'extra_html': ExtraHTML('html/item_list.html', {}),
- 'navigate_to': 'html/item_list.html'
-})
-@pytest.mark.usefixtures('webextension')
-@pytest.mark.parametrize('item_type', ['resource', 'mapping'])
-def test_item_list_displaying(driver, execute_in_page, item_type):
- """
- A test case of items list interaction with preview and dialog.
- """
- execute_in_page(load_script('html/item_list.js'))
-
- items = [make_item(item_type, f'item{i}', f'Item {i}') for i in range(3)]
-
- sample_data = {
- 'resource': {},
- 'mapping': {},
- 'file': {
- 'sha256': sample_files_by_sha256
- }
- }
- sample_data[item_type] = sample_data_dict(items)
-
- preview_container, dialog_container, ul = execute_in_page(
- f'''
- let list_ctx, sample_data = arguments[0];
- async function create_list() {{
- await haketilodb.save_items(sample_data);
- list_ctx = await {item_type}_list();
- document.body.append(list_ctx.main_div);
- return [list_ctx.preview_container, list_ctx.dialog_container,
- list_ctx.ul];
- }}
- returnval(create_list());
- ''',
- sample_data)
-
- assert not preview_container.is_displayed()
-
- # Check that preview is displayed correctly.
- for i in range(3):
- execute_in_page('list_ctx.ul.children[arguments[0]].click();', i)
- assert preview_container.is_displayed()
- text = preview_container.text
- assert f'item{i}' in text
- assert f'Item {i}' in text
-
- # Check that item removal confirmation dialog is displayed correctly.
- execute_in_page('list_ctx.remove_but.click();')
- WebDriverWait(driver, 10).until(lambda _: dialog_container.is_displayed())
- assert 'list_disabled' in ul.get_attribute('class')
- assert not preview_container.is_displayed()
- msg = execute_in_page('returnval(list_ctx.dialog_ctx.msg.textContent);')
- assert msg == "Are you sure you want to delete 'item2'?"
-
- # Check that previewing other item is impossible while dialog is open.
- execute_in_page('list_ctx.ul.children[0].click();')
- assert dialog_container.is_displayed()
- assert 'list_disabled' in ul.get_attribute('class')
- assert not preview_container.is_displayed()
-
- # Check that queuing multiple removal confirmation dialogs is impossible.
- execute_in_page('list_ctx.remove_but.click();')
-
- # Check that answering "No" causes the item not to be removed and unhides
- # item preview.
- execute_in_page('list_ctx.dialog_ctx.no_but.click();')
- WebDriverWait(driver, 10).until(lambda _: preview_container.is_displayed())
- assert not dialog_container.is_displayed()
- assert 'list_disabled' not in ul.get_attribute('class')
- assert execute_in_page('returnval(list_ctx.ul.children.length);') == 3
-
- # Check that item removal works properly.
- def remove_current_item():
- execute_in_page('list_ctx.remove_but.click();')
- WebDriverWait(driver, 10)\
- .until(lambda _: dialog_container.is_displayed())
- execute_in_page('list_ctx.dialog_ctx.yes_but.click();')
-
- remove_current_item()
-
- def item_deleted(driver):
- return execute_in_page('returnval(list_ctx.ul.children.length);') == 2
- WebDriverWait(driver, 10).until(item_deleted)
- assert not dialog_container.is_displayed()
- assert not preview_container.is_displayed()
- assert 'list_disabled' not in ul.get_attribute('class')
-
- execute_in_page('list_ctx.ul.children[1].click();')
-
- # Check that item removal failure causes the right error dialog to appear.
- execute_in_page('haketilodb.finalize_transaction = () => {throw "sth";};')
- remove_current_item()
- WebDriverWait(driver, 10).until(lambda _: dialog_container.is_displayed())
- msg = execute_in_page('returnval(list_ctx.dialog_ctx.msg.textContent);')
- assert msg == "Couldn't remove 'item1' :("
-
- # Destroy item list.
- assert True == execute_in_page(
- '''
- const main_div = list_ctx.main_div;
- destroy_list(list_ctx);
- returnval(main_div.parentElement === null);
- ''')
diff --git a/test/unit/test_item_preview.py b/test/unit/test_item_preview.py
deleted file mode 100644
index fe9a98e..0000000
--- a/test/unit/test_item_preview.py
+++ /dev/null
@@ -1,208 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - displaying resources and mappings details
-"""
-
-# 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
-from selenium.webdriver.support.ui import WebDriverWait
-from selenium.common.exceptions import NoSuchWindowException
-
-from ..extension_crafting import ExtraHTML
-from ..script_loader import load_script
-from .utils import *
-
-@pytest.mark.ext_data({
- 'extra_html': ExtraHTML('html/item_preview.html', {}),
- 'navigate_to': 'html/item_preview.html'
-})
-@pytest.mark.usefixtures('webextension')
-def test_resource_preview(driver, execute_in_page):
- """
- A test case of the resource preview display function.
- """
- execute_in_page(load_script('html/item_preview.js'))
-
- sample_resource = make_sample_resource()
-
- preview_div = execute_in_page(
- '''
- let preview_object = resource_preview(arguments[0]);
- document.body.append(preview_object.main_div);
- returnval(preview_object.main_div);
- ''',
- sample_resource)
- text = preview_div.text
-
- assert '...' not in text
-
- for string in [
- *filter(lambda v: type(v) is str, sample_resource.values()),
- *[rr['identifier'] for rr in sample_resource['dependencies']],
- *[c['file'] for k in ('source_copyright', 'scripts')
- for c in sample_resource[k]],
- item_version_string(sample_resource, True)
- ]:
- assert string in text
-
- sample_resource['identifier'] = 'hellopear'
- sample_resource['long_name'] = 'Hello Pear'
- sample_resource['description'] = 'greets a pear'
- sample_resource['dependencies'] = [{'identifier': 'hello-msg'}]
- for key in ('scripts', 'source_copyright'):
- for file_ref in sample_resource[key]:
- file_ref['file'] = file_ref['file'].replace('.', '_')
-
- preview_div = execute_in_page(
- '''
- returnval(resource_preview(arguments[0], preview_object).main_div);
- ''',
- sample_resource)
- text = preview_div.text
-
- for string in ['...', 'pple', 'hello-message', 'report.spdx',
- 'LICENSES/CC0-1.0.txt', 'hello.js', 'bye.js']:
- assert string not in text
-
- for string in ['hellopear', 'Hello Pear', 'hello-msg', 'greets a pear',
- 'report_spdx', 'LICENSES/CC0-1_0_txt', 'hello_js', 'bye_js']:
- assert string in text
-
-@pytest.mark.ext_data({
- 'extra_html': ExtraHTML('html/item_preview.html', {}),
- 'navigate_to': 'html/item_preview.html'
-})
-@pytest.mark.usefixtures('webextension')
-def test_mapping_preview(driver, execute_in_page):
- """
- A test case of the mapping preview display function.
- """
- execute_in_page(load_script('html/item_preview.js'))
-
- sample_mapping = make_sample_mapping()
-
- preview_div = execute_in_page(
- '''
- let preview_object = mapping_preview(arguments[0]);
- document.body.append(preview_object.main_div);
- returnval(preview_object.main_div);
- ''',
- sample_mapping)
- text = preview_div.text
-
- assert '...' not in text
-
- for string in [
- *filter(lambda v: type(v) is str, sample_mapping.values()),
- *[p['identifier'] for p in sample_mapping['payloads'].values()],
- *[c['file'] for c in sample_mapping['source_copyright']],
- item_version_string(sample_mapping)
- ]:
- assert string in text
-
- sample_mapping['identifier'] = 'example-org-bloated'
- sample_mapping['long_name'] = 'Example.org Bloated',
- sample_mapping['payloads'] = dict(
- [(pat.replace('.org', '.com'), res_id)
- for pat, res_id in sample_mapping['payloads'].items()]
- )
- for file_ref in sample_mapping['source_copyright']:
- file_ref['file'] = file_ref['file'].replace('.', '_')
-
- preview_div = execute_in_page(
- '''
- returnval(mapping_preview(arguments[0], preview_object).main_div);
- ''',
- sample_mapping)
- text = preview_div.text
-
- for string in ['...', 'inimal', 'example.org', 'report.spdx',
- 'LICENSES/CC0-1.0.txt']:
- assert string not in text
-
- for string in ['example-org-bloated', 'Example.org Bloated', 'example.com',
- 'report_spdx', 'LICENSES/CC0-1_0_txt']:
- assert string in text
-
-@pytest.mark.ext_data({
- 'background_script': broker_js,
- 'extra_html': [
- ExtraHTML('html/item_preview.html', {}),
- ExtraHTML('html/file_preview.html', {}, wrap_into_htmldoc=False)
- ],
- 'navigate_to': 'html/item_preview.html'
-})
-@pytest.mark.usefixtures('webextension')
-def test_file_preview_link(driver, execute_in_page):
- """
- A test case of <a> links created by preview functions that allow a
- referenced file to be previewed.
- """
- execute_in_page(load_script('html/item_preview.js'))
-
- sample_data = make_complete_sample_data()
- sample_data['mapping'] = {}
- execute_in_page('returnval(haketilodb.save_items(arguments[0]));',
- sample_data)
-
- # Cause the "link" to `bye.js` to be invalid.
- sample_resource = make_sample_resource()
- sample_resource['scripts'][1]['sha256'] = 'dummy nonexistent hash'
-
- execute_in_page(
- '''
- let resource_preview_object = resource_preview(arguments[0], undefined);
- document.body.append(resource_preview_object.main_div);
- ''',
- sample_resource)
-
- window0 = driver.window_handles[0]
- driver.find_element_by_link_text('hello.js').click()
-
- def blob_url_navigated(driver):
- if len(driver.window_handles) < 2:
- return
- window1 = [wh for wh in driver.window_handles if wh != window0][0]
- driver.switch_to.window(window1)
- try:
- return driver.current_url.startswith('blob')
- except NoSuchWindowException:
- pass
-
- WebDriverWait(driver, 10).until(blob_url_navigated)
-
- assert sample_files['hello.js']['contents'].strip() \
- in driver.find_element_by_tag_name("pre").text
-
- driver.close()
- driver.switch_to.window(window0)
-
- driver.find_element_by_link_text('bye.js').click()
-
- def get_error_span(driver):
- if len(driver.window_handles) < 2:
- return
- window1 = [wh for wh in driver.window_handles if wh != window0][0]
- driver.switch_to.window(window1)
- try:
- return driver.find_element_by_id('error_msg')
- except NoSuchWindowException:
- pass
-
- error_span = WebDriverWait(driver, 10).until(get_error_span)
- assert error_span.is_displayed()
- assert "Couldn't find file in Haketilo's internal database :(" \
- in error_span.text
diff --git a/test/unit/test_patterns.py b/test/unit/test_patterns.py
deleted file mode 100644
index f2eeaf8..0000000
--- a/test/unit/test_patterns.py
+++ /dev/null
@@ -1,152 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - URL patterns
-"""
-
-# This file is part of Haketilo
-#
-# Copyright (C) 2021, Wojtek Kosior
-#
-# 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
-
-from ..script_loader import load_script
-
-@pytest.mark.get_page('https://gotmyowndoma.in')
-def test_regexes(execute_in_page):
- """
- patterns.js contains regexes used for URL parsing.
- Verify they work properly.
- """
- execute_in_page(load_script('common/patterns.js'))
-
- valid_url = 'https://example.com/a/b?ver=1.2.3#heading2'
- valid_url_rest = 'example.com/a/b?ver=1.2.3#heading2'
-
- # Test matching of URL protocol.
- match = execute_in_page('returnval(proto_regex.exec(arguments[0]));',
- valid_url)
- assert match
- assert match[1] == 'https'
- assert match[2] == valid_url_rest
-
- match = execute_in_page('returnval(proto_regex.exec(arguments[0]));',
- '://bad-url.missing/protocol')
- assert match is None
-
- # Test matching of http(s) URLs.
- match = execute_in_page('returnval(http_regex.exec(arguments[0]));',
- valid_url_rest)
- assert match
- assert match[1] == 'example.com'
- assert match[2] == '/a/b'
- assert match[3] == '?ver=1.2.3'
-
- match = execute_in_page('returnval(http_regex.exec(arguments[0]));',
- 'another.example.com')
- assert match
- assert match[1] == 'another.example.com'
- assert match[2] == ''
- assert match[3] == ''
-
- match = execute_in_page('returnval(http_regex.exec(arguments[0]));',
- '/bad/http/example')
- assert match == None
-
- # Test matching of file URLs.
- match = execute_in_page('returnval(file_regex.exec(arguments[0]));',
- '/good/file/example')
- assert match
- assert match[1] == '/good/file/example'
-
- # Test matching of ftp URLs.
- match = execute_in_page('returnval(ftp_regex.exec(arguments[0]));',
- 'example.com/a/b#heading2')
- assert match
- assert match[1] is None
- assert match[2] == 'example.com'
- assert match[3] == '/a/b'
-
- match = execute_in_page('returnval(ftp_regex.exec(arguments[0]));',
- 'some_user@localhost')
- assert match
- assert match[1] == 'some_user@'
- assert match[2] == 'localhost'
- assert match[3] == ''
-
- match = execute_in_page('returnval(ftp_regex.exec(arguments[0]));',
- '@bad.url/')
- assert match is None
-
-@pytest.mark.get_page('https://gotmyowndoma.in')
-def test_deconstruct_url(execute_in_page):
- """
- patterns.js contains deconstruct_url() function that handles URL parsing.
- Verify it works properly.
- """
- execute_in_page(load_script('common/patterns.js'))
-
- deco = execute_in_page('returnval(deconstruct_url(arguments[0]));',
- 'https://eXaMpLe.com/a/b?ver=1.2.3#heading2')
- assert deco
- assert deco['trailing_slash'] == False
- assert deco['proto'] == 'https'
- assert deco['domain'] == ['example', 'com']
- assert deco['path'] == ['a', 'b']
-
- deco = execute_in_page('returnval(deconstruct_url(arguments[0]));',
- 'http://**.example.com/')
- assert deco
- assert deco['trailing_slash'] == True
- assert deco['proto'] == 'http'
- assert deco['domain'] == ['**', 'example', 'com']
- assert deco['path'] == []
-
- deco = execute_in_page('returnval(deconstruct_url(arguments[0]));',
- 'ftp://user@ftp.example.com/all///passwords.txt/')
- assert deco
- assert deco['trailing_slash'] == True
- assert deco['proto'] == 'ftp'
- assert deco['domain'] == ['ftp', 'example', 'com']
- assert deco['path'] == ['all', 'passwords.txt']
-
- deco = execute_in_page('returnval(deconstruct_url(arguments[0]));',
- 'ftp://mirror.edu.pl.eu.org')
- assert deco
- assert deco['trailing_slash'] == False
- assert deco['proto'] == 'ftp'
- assert deco['domain'] == ['mirror', 'edu', 'pl', 'eu', 'org']
- assert deco['path'] == []
-
- deco = execute_in_page('returnval(deconstruct_url(arguments[0]));',
- 'file:///mnt/parabola_chroot///etc/passwd')
- assert deco
- assert deco['trailing_slash'] == False
- assert deco['proto'] == 'file'
- assert deco['path'] == ['mnt', 'parabola_chroot', 'etc', 'passwd']
- assert 'domain' not in deco
-
- for bad_url in [
- '://bad-url.missing/protocol',
- 'http:/example.com/a/b',
- 'unknown://example.com/a/b',
- 'idontfancypineapple',
- 'ftp://@example.org/',
- 'https:///some/path/',
- 'file://non-absolute/path'
- ]:
- with pytest.raises(Exception, match=r'Error in injected script'):
- deco = execute_in_page('returnval(deconstruct_url(arguments[0]));',
- bad_url)
-
- # at some point we might also consider testing url deconstruction with
- # length limits...
diff --git a/test/unit/test_patterns_query_manager.py b/test/unit/test_patterns_query_manager.py
deleted file mode 100644
index 9fbc438..0000000
--- a/test/unit/test_patterns_query_manager.py
+++ /dev/null
@@ -1,307 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - building pattern tree and putting it in a content script
-"""
-
-# This file is part of Haketilo
-#
-# Copyright (C) 2021,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 selenium.common.exceptions import TimeoutException
-
-from ..script_loader import load_script
-
-def simple_sample_mapping(patterns, fruit):
- if type(patterns) is not list:
- patterns = [patterns]
- payloads = dict([(p, {'identifier': f'{fruit}-{p}'}) for p in patterns])
- return {
- 'source_copyright': [],
- 'type': 'mapping',
- 'identifier': f'inject-{fruit}',
- 'payloads': payloads
- }
-
-def get_content_script_values(driver, content_script):
- """
- Allow easy extraction of 'this.something = ...' values from generated
- content script and verify the content script is syntactically correct.
- """
- return driver.execute_script(
- '''
- function value_holder() {
- %s;
- return this;
- }
- return value_holder.call({});
- ''' % content_script)
-
-# Fields that are not relevant for testing are omitted from these mapping
-# definitions.
-sample_mappings = [simple_sample_mapping(pats, fruit) for pats, fruit in [
- (['https://gotmyowndoma.in/index.html',
- 'http://gotmyowndoma.in/index.html'], 'banana'),
- (['https://***.gotmyowndoma.in/index.html',
- 'https://**.gotmyowndoma.in/index.html',
- 'https://*.gotmyowndoma.in/index.html',
- 'https://gotmyowndoma.in/index.html'], 'orange'),
- ('https://gotmyowndoma.in/index.html/***', 'grape'),
- ('http://gotmyowndoma.in/index.html/***', 'melon'),
- ('https://gotmyowndoma.in/index.html', 'peach'),
- ('https://gotmyowndoma.in/*', 'pear'),
- ('https://gotmyowndoma.in/**', 'raspberry'),
- ('https://gotmyowndoma.in/***', 'strawberry'),
- ('https://***.gotmyowndoma.in/index.html', 'apple'),
- ('https://***.gotmyowndoma.in/*', 'avocado'),
- ('https://***.gotmyowndoma.in/**', 'papaya'),
- ('https://***.gotmyowndoma.in/***', 'kiwi')
-]]
-
-sample_blocking = [f'http{s}://{dw}gotmyown%sdoma.in{i}{pw}'
- for dw in ('', '***.', '**.', '*.')
- for i in ('/index.html', '')
- for pw in ('', '/', '/*')
- for s in ('', 's')]
-sample_blocking = [{'pattern': pattern % (i if i > 1 else ''),
- 'allow': bool(i & 1)}
- for i, pattern in enumerate(sample_blocking)]
-
-# Even though patterns_query_manager.js is normally meant to run from background
-# page, some tests can be as well performed running it from a normal page.
-@pytest.mark.get_page('https://gotmyowndoma.in')
-def test_pqm_tree_building(driver, execute_in_page):
- """
- patterns_query_manager.js tracks Haketilo's internal database and builds a
- constantly-updated pattern tree based on its contents. Mock the database and
- verify tree building works properly.
- """
- execute_in_page(load_script('background/patterns_query_manager.js'))
- # Mock IndexedDB and build patterns tree.
- execute_in_page(
- '''
- const [initial_mappings, initial_blocking] = arguments.slice(0, 2);
- let mappingchange, blockingchange, settingchange;
-
- haketilodb.track.mapping = function (cb) {
- mappingchange = cb;
-
- return [{}, initial_mappings];
- }
- haketilodb.track.blocking = function (cb) {
- blockingchange = cb;
-
- return [{}, initial_blocking];
- }
- haketilodb.track.setting = function (cb) {
- settingchange = cb;
-
- return [{}, [{name: "default_allow", value: true}]];
- }
-
- let last_script;
- let unregister_called = 0;
- async function register_mock(injection)
- {
- await new Promise(resolve => setTimeout(resolve, 1));
- last_script = injection.js[0].code;
- return {unregister: () => unregister_called++};
- }
- browser = {contentScripts: {register: register_mock}};
-
- returnval(start("abracadabra"));
- ''',
- sample_mappings[0:2], sample_blocking[0:2])
-
- found, tree, content_script, deregistrations = execute_in_page(
- '''
- returnval([pqt.search(tree, arguments[0]).next().value,
- tree, last_script, unregister_called]);
- ''',
- 'https://gotmyowndoma.in/index.html')
- best_pattern = 'https://gotmyowndoma.in/index.html'
- assert found == \
- dict([('~allow', 1),
- *[(f'inject-{fruit}', {'identifier': f'{fruit}-{best_pattern}'})
- for fruit in ('banana', 'orange')]])
- cs_values = get_content_script_values(driver, content_script)
- assert cs_values['haketilo_secret'] == 'abracadabra'
- assert cs_values['haketilo_pattern_tree'] == tree
- assert cs_values['haketilo_default_allow'] == True
- assert deregistrations == 0
-
- def condition_all_added(driver):
- last_script = execute_in_page('returnval(last_script);')
- cs_values = get_content_script_values(driver, last_script)
- nums = [i for i in range(len(sample_blocking)) if i > 1]
- return (cs_values['haketilo_default_allow'] == False and
- all([('gotmyown%sdoma' % i) in last_script for i in nums]) and
- all([m['identifier'] in last_script for m in sample_mappings]))
-
- execute_in_page(
- '''{
- const new_setting_val = {name: "default_allow", value: false};
- settingchange({key: "default_allow", new_val: new_setting_val});
- for (const mapping of arguments[0])
- mappingchange({key: mapping.identifier, new_val: mapping});
- for (const blocking of arguments[1])
- blockingchange({key: blocking.pattern, new_val: blocking});
- }''',
- sample_mappings[2:], sample_blocking[2:])
- WebDriverWait(driver, 10).until(condition_all_added)
-
- odd_mappings = \
- [m['identifier'] for i, m in enumerate(sample_mappings) if i & 1]
- odd_blocking = \
- [b['pattern'] for i, b in enumerate(sample_blocking) if i & 1]
- even_mappings = \
- [m['identifier'] for i, m in enumerate(sample_mappings) if 1 - i & 1]
- even_blocking = \
- [b['pattern'] for i, b in enumerate(sample_blocking) if 1 - i & 1]
-
- def condition_odd_removed(driver):
- last_script = execute_in_page('returnval(last_script);')
- nums = [i for i in range(len(sample_blocking)) if i > 1 and 1 - i & 1]
- return (all([id not in last_script for id in odd_mappings]) and
- all([id in last_script for id in even_mappings]) and
- all([p not in last_script for p in odd_blocking[1:]]) and
- all([('gotmyown%sdoma' % i) in last_script for i in nums]))
-
- def condition_all_removed(driver):
- content_script = execute_in_page('returnval(last_script);')
- cs_values = get_content_script_values(driver, content_script)
- return cs_values['haketilo_pattern_tree'] == {}
-
- execute_in_page(
- '''
- arguments[0].forEach(identifier => mappingchange({key: identifier}));
- arguments[1].forEach(pattern => blockingchange({key: pattern}));
- ''',
- odd_mappings, odd_blocking)
-
- WebDriverWait(driver, 10).until(condition_odd_removed)
-
- execute_in_page(
- '''
- arguments[0].forEach(identifier => mappingchange({key: identifier}));
- arguments[1].forEach(pattern => blockingchange({key: pattern}));
- ''',
- even_mappings, even_blocking)
-
- WebDriverWait(driver, 10).until(condition_all_removed)
-
- def condition_default_allowed_again(driver):
- content_script = execute_in_page('returnval(last_script);')
- cs_values = get_content_script_values(driver, content_script)
- return cs_values['haketilo_default_allow'] == True
-
- execute_in_page(
- '''{
- const new_setting_val = {name: "default_allow", value: true};
- settingchange({key: "default_allow", new_val: new_setting_val});
- }''')
-
- WebDriverWait(driver, 10).until(condition_default_allowed_again)
-
-content_js = '''
-let already_run = false;
-this.haketilo_content_script_main = function() {
- if (already_run)
- return;
- already_run = true;
- document.documentElement.innerHTML = "<body><div id='tree-json'>";
- document.getElementById("tree-json").innerText =
- JSON.stringify(this.haketilo_pattern_tree);
-}
-if (this.haketilo_pattern_tree !== undefined)
- this.haketilo_content_script_main();
-'''
-
-def background_js():
- pqm_js = load_script('background/patterns_query_manager.js',
- "#IMPORT background/broadcast_broker.js")
- return pqm_js + '; broadcast_broker.start(); start();'
-
-@pytest.mark.ext_data({
- 'content_script': content_js,
- 'background_script': background_js
-})
-@pytest.mark.usefixtures('webextension')
-def test_pqm_script_injection(driver, execute_in_page):
- # Let's open a normal page in a second window. Window 0 will be used to make
- # changes to IndexedDB and window 1 to test the working of content scripts.
- driver.execute_script('window.open("about:blank", "_blank");')
- WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) == 2)
- windows = [*driver.window_handles]
-
- def get_tree_json(driver):
- return driver.execute_script(
- '''
- return (document.getElementById("tree-json") || {}).innerText;
- ''')
-
- def run_content_script():
- driver.switch_to.window(windows[1])
- driver.get('https://gotmyowndoma.in/index.html')
- windows[1] = driver.current_window_handle
- try:
- return WebDriverWait(driver, 10).until(get_tree_json)
- except TimeoutException:
- pass
-
- for attempt in range(2):
- json_txt = run_content_script()
- if json_txt and json.loads(json_txt) == {}:
- break;
- assert attempt != 1
-
- driver.switch_to.window(windows[0])
- execute_in_page(load_script('common/indexeddb.js'))
-
- sample_data = {
- 'mapping': dict([(sm['identifier'], {'1.0': sm})
- for sm in sample_mappings]),
- 'resource': {},
- 'file': {}
- }
- execute_in_page('returnval(save_items(arguments[0]));', sample_data)
-
- for attempt in range(2):
- tree_json = run_content_script() or '{}'
- json.loads(tree_json)
- if all([m['identifier'] in tree_json for m in sample_mappings]):
- break
- assert attempt != 1
-
- driver.switch_to.window(windows[0])
- execute_in_page(
- '''{
- const identifiers = arguments[0];
- async function remove_items()
- {
- const ctx = await start_items_transaction(["mapping"], {});
- for (const id of identifiers)
- await remove_mapping(id, ctx);
- await finalize_transaction(ctx);
- }
- returnval(remove_items());
- }''',
- [sm['identifier'] for sm in sample_mappings])
-
- for attempt in range(2):
- json_txt = run_content_script()
- if json_txt and json.loads(json_txt) == {}:
- break;
- assert attempt != 1
diff --git a/test/unit/test_patterns_query_tree.py b/test/unit/test_patterns_query_tree.py
deleted file mode 100644
index 80bf554..0000000
--- a/test/unit/test_patterns_query_tree.py
+++ /dev/null
@@ -1,474 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - URL patterns
-"""
-
-# This file is part of Haketilo
-#
-# Copyright (C) 2021, Wojtek Kosior
-#
-# 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
-
-from ..script_loader import load_script
-
-@pytest.mark.get_page('https://gotmyowndoma.in')
-def test_modify_branch(execute_in_page):
- """
- patterns_query_tree.js contains Pattern Tree data structure that allows
- arrays of string labels to be mapped to items.
- Verify operations modifying a single branch of such tree work properly.
- """
- execute_in_page(load_script('common/patterns_query_tree.js'))
- execute_in_page(
- '''
- let items_added;
- let items_removed;
-
- function _item_adder(item, array)
- {
- items_added++;
- return [...(array || []), item];
- }
-
- function item_adder(item)
- {
- items_added = 0;
- return array => _item_adder(item, array);
- }
-
- function _item_remover(array)
- {
- if (array !== null) {
- items_removed++;
- array.pop();
- }
- return (array && array.length > 0) ? array : null;
- }
-
- function item_remover()
- {
- items_removed = 0;
- return _item_remover;
- }''')
-
- # Let's construct some tree branch while checking that each addition gives
- # the right result.
- branch = execute_in_page(
- '''{
- const branch = empty_node();
- modify_sequence(branch, ['com', 'example'], item_adder('some_item'));
- returnval(branch);
- }''')
- assert branch == {
- 'literal_match': None,
- 'wildcard_matches': [None, None, None],
- 'children': {
- 'com': {
- 'literal_match': None,
- 'wildcard_matches': [None, None, None],
- 'children': {
- 'example': {
- 'literal_match': ['some_item'],
- 'wildcard_matches': [None, None, None],
- 'children': {
- }
- }
- }
- }
- }
- }
-
- branch, items_added = execute_in_page(
- '''{
- const branch = arguments[0];
- modify_sequence(branch, ['com', 'example'], item_adder('other_item'));
- returnval([branch, items_added]);
- }''', branch)
- assert items_added == 1
- assert branch['children']['com']['children']['example']['literal_match'] \
- == ['some_item', 'other_item']
-
- for i in range(3):
- for expected_array in [['third_item'], ['third_item', '4th_item']]:
- wildcard = '*' * (i + 1)
- branch, items_added = execute_in_page(
- '''{
- const branch = arguments[0];
- modify_sequence(branch, ['com', 'sample', arguments[1]],
- item_adder(arguments[2]));
- returnval([branch, items_added]);
- }''',
- branch, wildcard, expected_array[-1])
- assert items_added == 2
- sample = branch['children']['com']['children']['sample']
- assert sample['wildcard_matches'][i] == expected_array
- assert sample['children'][wildcard]['literal_match'] \
- == expected_array
-
- branch, items_added = execute_in_page(
- '''{
- const branch = arguments[0];
- modify_sequence(branch, ['org', 'koszko', '***', '123'],
- item_adder('5th_item'));
- returnval([branch, items_added]);
- }''',
- branch)
- assert items_added == 1
- assert branch['children']['org']['children']['koszko']['children']['***']\
- ['children']['123']['literal_match'] == ['5th_item']
-
- # Let's verify that removing a nonexistent element doesn't modify the tree.
- branch2, items_removed = execute_in_page(
- '''{
- const branch = arguments[0];
- modify_sequence(branch, ['com', 'not', 'registered', '*'],
- item_remover());
- returnval([branch, items_removed]);
- }''',
- branch)
- assert branch == branch2
- assert items_removed == 0
-
- # Let's remove all elements in the tree branch while checking that each
- # removal gives the right result.
- branch, items_removed = execute_in_page(
- '''{
- const branch = arguments[0];
- modify_sequence(branch, ['org', 'koszko', '***', '123'],
- item_remover());
- returnval([branch, items_removed]);
- }''',
- branch)
- assert items_removed == 1
- assert 'org' not in branch['children']
-
- for i in range(3):
- for expected_array in [['third_item'], None]:
- wildcard = '*' * (i + 1)
- branch, items_removed = execute_in_page(
- '''{
- const branch = arguments[0];
- modify_sequence(branch, ['com', 'sample', arguments[1]],
- item_remover());
- returnval([branch, items_removed]);
- }''',
- branch, wildcard)
- assert items_removed == 2
- if i == 2 and expected_array == []:
- break
- sample = branch['children']['com']['children'].get('sample', {})
- assert sample.get('wildcard_matches', [None, None, None])[i] \
- == expected_array
- assert sample.get('children', {}).get(wildcard, {})\
- .get('literal_match') == expected_array
-
- for i in range(2):
- branch, items_removed = execute_in_page(
- '''{
- const branch = arguments[0];
- modify_sequence(branch, ['com', 'example'], item_remover());
- returnval([branch, items_removed]);
- }''',
- branch)
- assert items_removed == 1
- if i == 0:
- assert branch['children']['com']['children']['example']\
- ['literal_match'] == ['some_item']
- else:
- assert branch == {
- 'literal_match': None,
- 'wildcard_matches': [None, None, None],
- 'children': {
- }
- }
-
-@pytest.mark.get_page('https://gotmyowndoma.in')
-def test_search_branch(execute_in_page):
- """
- patterns_query_tree.js contains Pattern Tree data structure that allows
- arrays of string labels to be mapped to items.
- Verify searching a single branch of such tree work properly.
- """
- execute_in_page(load_script('common/patterns_query_tree.js'))
- execute_in_page(
- '''
- const item_adder = item => (array => [...(array || []), item]);
- ''')
-
- # Let's construct some tree branch to test on.
- execute_in_page(
- '''
- var branch = empty_node();
-
- for (const [item, sequence] of [
- ['(root)', []],
- ['***', ['***']],
- ['**', ['**']],
- ['*', ['*']],
-
- ['a', ['a']],
- ['A', ['a']],
- ['b', ['b']],
-
- ['a/***', ['a', '***']],
- ['A/***', ['a', '***']],
- ['a/**', ['a', '**']],
- ['A/**', ['a', '**']],
- ['a/*', ['a', '*']],
- ['A/*', ['a', '*']],
- ['a/sth', ['a', 'sth']],
- ['A/sth', ['a', 'sth']],
-
- ['b/***', ['b', '***']],
- ['b/**', ['b', '**']],
- ['b/*', ['b', '*']],
- ['b/sth', ['b', 'sth']],
- ])
- modify_sequence(branch, sequence, item_adder(item));
- ''')
-
- # Let's make the actual searches on our testing branch.
- for sequence, expected in [
- ([], [{'(root)'}, {'***'}]),
- (['a'], [{'a', 'A'}, {'a/***', 'A/***'}, {'*'}, {'***'}]),
- (['b'], [{'b'}, {'b/***'}, {'*'}, {'***'}]),
- (['c'], [ {'*'}, {'***'}]),
- (['***'], [{'***'}, {'*'} ]),
- (['**'], [{'**'}, {'*'}, {'***'}]),
- (['**'], [{'**'}, {'*'}, {'***'}]),
- (['*'], [{'*'}, {'***'}]),
-
- (['a', 'sth'], [{'a/sth', 'A/sth'}, {'a/*', 'A/*'}, {'a/***', 'A/***'}, {'**'}, {'***'}]),
- (['b', 'sth'], [{'b/sth'}, {'b/*'}, {'b/***'}, {'**'}, {'***'}]),
- (['a', 'hts'], [ {'a/*', 'A/*'}, {'a/***', 'A/***'}, {'**'}, {'***'}]),
- (['b', 'hts'], [ {'b/*'}, {'b/***'}, {'**'}, {'***'}]),
- (['a', '***'], [{'a/***', 'A/***'}, {'a/*', 'A/*'}, {'**'}, {'***'}]),
- (['b', '***'], [{'b/***'}, {'b/*'}, {'**'}, {'***'}]),
- (['a', '**'], [{'a/**', 'A/**'}, {'a/*', 'A/*'}, {'a/***', 'A/***'}, {'**'}, {'***'}]),
- (['b', '**'], [{'b/**'}, {'b/*'}, {'b/***'}, {'**'}, {'***'}]),
- (['a', '*'], [{'a/*', 'A/*'}, {'a/***', 'A/***'}, {'**'}, {'***'}]),
- (['b', '*'], [{'b/*'}, {'b/***'}, {'**'}, {'***'}]),
-
- (['a', 'c', 'd'], [{'a/**', 'A/**'}, {'a/***', 'A/***'}, {'**'}, {'***'}]),
- (['b', 'c', 'd'], [{'b/**'}, {'b/***'}, {'**'}, {'***'}])
- ]:
- result = execute_in_page(
- '''
- returnval([...search_sequence(branch, arguments[0])]);
- ''',
- sequence)
-
- try:
- assert len(result) == len(expected)
-
- for expected_set, result_array in zip(expected, result):
- assert len(expected_set) == len(result_array)
- assert expected_set == set(result_array)
- except Exception as e:
- import sys
- print('sequence:', sequence, '\nexpected:', expected,
- '\nresult:', result, file=sys.stderr)
- raise e from None
-
-@pytest.mark.get_page('https://gotmyowndoma.in')
-def test_pattern_tree(execute_in_page):
- """
- patterns_query_tree.js contains Pattern Tree data structure that allows
- arrays of string labels to be mapped to items.
- Verify operations on entire such tree work properly.
- """
- execute_in_page(load_script('common/patterns_query_tree.js'))
-
- # Perform tests with all possible patterns for a simple URL.
- url = 'https://example.com'
- patterns = [
- 'https://example.com',
- 'https://example.com/***',
- 'https://***.example.com',
- 'https://***.example.com/***'
- ]
- bad_patterns = [
- 'http://example.com',
- 'https://a.example.com',
- 'https://*.example.com',
- 'https://**.example.com',
- 'https://example.com/a',
- 'https://example.com/*',
- 'https://example.com/**',
- ]
-
- expected = [{'key': p} for p in patterns]
-
- tree, result = execute_in_page(
- '''{
- const tree = pattern_tree_make();
- for (const pattern of arguments[0].concat(arguments[1])) {
- pattern_tree_register(tree, pattern, 'key', pattern);
- pattern_tree_register(tree, pattern + '/', 'key', pattern + '/');
- }
- returnval([tree, [...pattern_tree_search(tree, arguments[2])]]);
- }''',
- patterns, bad_patterns, url)
- assert expected == result
-
- # Also verify that deregistering half of the good patterns works correctly.
- patterns_removed = [pattern for i, pattern in enumerate(patterns) if i % 2]
- patterns = [pattern for i, pattern in enumerate(patterns) if not (i % 2)]
- expected = [{'key': p} for p in patterns]
- tree, result = execute_in_page(
- '''{
- const tree = arguments[0];
- for (const pattern of arguments[1]) {
- pattern_tree_deregister(tree, pattern, 'key');
- pattern_tree_deregister(tree, pattern + '/', 'key');
- }
- returnval([tree, [...pattern_tree_search(tree, arguments[2])]]);
- }''',
- tree, patterns_removed, url)
- assert expected == result
-
- # Also verify that deregistering all the patterns works correctly.
- tree = execute_in_page(
- '''{
- const tree = arguments[0];
- for (const pattern of arguments[1].concat(arguments[2])) {
- pattern_tree_deregister(tree, pattern, 'key');
- pattern_tree_deregister(tree, pattern + '/', 'key');
- }
- returnval(tree);
- }''',
- tree, patterns, bad_patterns)
- assert tree == {}
-
- # Perform tests with all possible patterns for a complex URL.
- url = 'http://settings.query.example.com/google/tries/destroy/adblockers//'
- patterns = [
- 'http://settings.query.example.com/google/tries/destroy/adblockers',
- 'http://settings.query.example.com/google/tries/destroy/adblockers/***',
- 'http://settings.query.example.com/google/tries/destroy/*',
- 'http://settings.query.example.com/google/tries/destroy/***',
- 'http://settings.query.example.com/google/tries/**',
- 'http://settings.query.example.com/google/tries/***',
- 'http://settings.query.example.com/google/**',
- 'http://settings.query.example.com/google/***',
- 'http://settings.query.example.com/**',
- 'http://settings.query.example.com/***',
-
- 'http://***.settings.query.example.com/google/tries/destroy/adblockers',
- 'http://***.settings.query.example.com/google/tries/destroy/adblockers/***',
- 'http://***.settings.query.example.com/google/tries/destroy/*',
- 'http://***.settings.query.example.com/google/tries/destroy/***',
- 'http://***.settings.query.example.com/google/tries/**',
- 'http://***.settings.query.example.com/google/tries/***',
- 'http://***.settings.query.example.com/google/**',
- 'http://***.settings.query.example.com/google/***',
- 'http://***.settings.query.example.com/**',
- 'http://***.settings.query.example.com/***',
- 'http://*.query.example.com/google/tries/destroy/adblockers',
- 'http://*.query.example.com/google/tries/destroy/adblockers/***',
- 'http://*.query.example.com/google/tries/destroy/*',
- 'http://*.query.example.com/google/tries/destroy/***',
- 'http://*.query.example.com/google/tries/**',
- 'http://*.query.example.com/google/tries/***',
- 'http://*.query.example.com/google/**',
- 'http://*.query.example.com/google/***',
- 'http://*.query.example.com/**',
- 'http://*.query.example.com/***',
- 'http://***.query.example.com/google/tries/destroy/adblockers',
- 'http://***.query.example.com/google/tries/destroy/adblockers/***',
- 'http://***.query.example.com/google/tries/destroy/*',
- 'http://***.query.example.com/google/tries/destroy/***',
- 'http://***.query.example.com/google/tries/**',
- 'http://***.query.example.com/google/tries/***',
- 'http://***.query.example.com/google/**',
- 'http://***.query.example.com/google/***',
- 'http://***.query.example.com/**',
- 'http://***.query.example.com/***',
- 'http://**.example.com/google/tries/destroy/adblockers',
- 'http://**.example.com/google/tries/destroy/adblockers/***',
- 'http://**.example.com/google/tries/destroy/*',
- 'http://**.example.com/google/tries/destroy/***',
- 'http://**.example.com/google/tries/**',
- 'http://**.example.com/google/tries/***',
- 'http://**.example.com/google/**',
- 'http://**.example.com/google/***',
- 'http://**.example.com/**',
- 'http://**.example.com/***',
- 'http://***.example.com/google/tries/destroy/adblockers',
- 'http://***.example.com/google/tries/destroy/adblockers/***',
- 'http://***.example.com/google/tries/destroy/*',
- 'http://***.example.com/google/tries/destroy/***',
- 'http://***.example.com/google/tries/**',
- 'http://***.example.com/google/tries/***',
- 'http://***.example.com/google/**',
- 'http://***.example.com/google/***',
- 'http://***.example.com/**',
- 'http://***.example.com/***'
- ]
- bad_patterns = [
- 'https://settings.query.example.com/google/tries/destroy/adblockers',
- 'http://settings.query.example.com/google/tries/destroy/adblockers/a',
- 'http://settings.query.example.com/google/tries/destroy/adblockers/*',
- 'http://settings.query.example.com/google/tries/destroy/adblockers/**',
- 'http://settings.query.example.com/google/tries/destroy/a',
- 'http://settings.query.example.com/google/tries/destroy/**',
- 'http://settings.query.example.com/google/tries/*',
- 'http://a.settings.query.example.com/google/tries/destroy/adblockers',
- 'http://*.settings.query.example.com/google/tries/destroy/adblockers',
- 'http://**.settings.query.example.com/google/tries/destroy/adblockers',
- 'http://a.query.example.com/google/tries/destroy/adblockers',
- 'http://**.query.example.com/google/tries/destroy/adblockers',
- 'http://*.example.com/google/tries/destroy/adblockers'
- ]
-
- expected = [{'key': p + s} for p in patterns for s in ['/', '']]
-
- tree, result = execute_in_page(
- '''{
- const tree = pattern_tree_make();
- for (const pattern of arguments[0].concat(arguments[1])) {
- pattern_tree_register(tree, pattern, 'key', pattern);
- pattern_tree_register(tree, pattern + '/', 'key', pattern + '/');
- }
- returnval([tree, [...pattern_tree_search(tree, arguments[2])]]);
- }''',
- patterns, bad_patterns, url)
- assert expected == result
-
- # Also verify that deregistering all patterns with trailing slash works
- # correctly.
- expected = [{'key': p} for p in patterns]
- tree, result = execute_in_page(
- '''{
- const tree = arguments[0];
- for (const pattern of arguments[1])
- pattern_tree_deregister(tree, pattern + '/', 'key');
- returnval([tree, [...pattern_tree_search(tree, arguments[2])]]);
- }''',
- tree, patterns, url)
- assert expected == result
-
- # Also verify that deregistering all the patterns works correctly.
- tree = execute_in_page(
- '''{
- const tree = arguments[0];
- for (const pattern of arguments[1])
- pattern_tree_deregister(tree, pattern, 'key');
- for (const pattern of arguments[2]) {
- pattern_tree_deregister(tree, pattern, 'key');
- pattern_tree_deregister(tree, pattern + '/', 'key');
- }
- returnval(tree);
- }''',
- tree, patterns, bad_patterns)
- assert tree == {}
diff --git a/test/unit/test_payload_create.py b/test/unit/test_payload_create.py
deleted file mode 100644
index 9689c37..0000000
--- a/test/unit/test_payload_create.py
+++ /dev/null
@@ -1,248 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - using a form to create simple site payload
-"""
-
-# 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 re
-from hashlib import sha256
-
-from selenium.webdriver.support.ui import WebDriverWait
-
-from ..extension_crafting import ExtraHTML
-from ..script_loader import load_script
-from .utils import *
-
-uuidv4_re = re.compile(
- r'^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$',
- re.IGNORECASE
-)
-
-sample_patterns = '''
-http://example.com/***
-
-https://*.example.org/**'''
-
-sample_form_data = {
- 'identifier': 'someid',
- 'long_name': 'Some Name',
- 'description': 'blah blah blah',
- 'patterns': sample_patterns,
- 'script': sample_files['hello.js']['contents']
-}
-
-def fill_form_with_sample_data(execute_in_page, sample_data_override={},
- form_ctx='form_ctx'):
- form_data = sample_form_data.copy()
- form_data.update(sample_data_override)
- execute_in_page(
- f'''
- for (const [key, value] of Object.entries(arguments[0]))
- {form_ctx}[key].value = value;
- ''',
- form_data)
- return form_data
-
-cleared_form_inputs = {
- 'identifier': '',
- 'long_name': '',
- 'description': '',
- 'patterns': 'https://example.com/***',
- 'script': 'console.log("Hello, World!");'
-}
-def assert_form_contents(execute_in_page, inputs=cleared_form_inputs):
- inputs_keys = [*inputs.keys()]
- values = execute_in_page(
- 'returnval(arguments[0].map(i => form_ctx[i].value));',
- inputs_keys
- )
- for key, value in zip(inputs_keys, values):
- assert inputs[key] == value
-
-@pytest.mark.ext_data({
- 'background_script': broker_js,
- 'extra_html': ExtraHTML('html/payload_create.html', {}),
- 'navigate_to': 'html/payload_create.html'
-})
-@pytest.mark.usefixtures('webextension')
-def test_payload_create_normal_usage(driver, execute_in_page):
- """
- A test case of normal usage of simple payload creation form.
- """
- execute_in_page(load_script('html/payload_create.js'))
-
- create_but, form_container, dialog_container = execute_in_page(
- '''
- const form_ctx = payload_create_form();
- document.body.append(form_ctx.main_div);
- returnval([form_ctx.create_but, form_ctx.form_container,
- form_ctx.dialog_container]);
- ''')
-
- assert patterns_doc_url == \
- driver.find_element_by_link_text('URL patterns').get_attribute('href')
-
- assert form_container.is_displayed()
- assert not dialog_container.is_displayed()
-
- assert_form_contents(execute_in_page)
-
- form_data = fill_form_with_sample_data(execute_in_page)
-
- create_but.click()
-
- assert not form_container.is_displayed()
- assert dialog_container.is_displayed()
-
- def success_reported(driver):
- return 'Successfully saved payload' in dialog_container.text
-
- WebDriverWait(driver, 10).until(success_reported)
- execute_in_page('form_ctx.dialog_ctx.ok_but.click();')
-
- assert form_container.is_displayed()
- assert not dialog_container.is_displayed()
-
- def assert_db_contents():
- db_contents = get_db_contents(execute_in_page)
-
- assert uuidv4_re.match(db_contents['resource'][0]['uuid'])
-
- localid = f'local-{form_data["identifier"]}'
- long_name = form_data['long_name'] or form_data['identifier']
- payloads = dict([(pat, {'identifier': localid})
- for pat in form_data['patterns'].split('\n') if pat])
-
- assert db_contents['resource'] == [{
- 'source_name': localid,
- 'source_copyright': [],
- 'type': 'resource',
- 'identifier': localid,
- 'uuid': db_contents['resource'][0]['uuid'],
- 'version': [1],
- 'description': form_data['description'],
- 'dependencies': [],
- 'long_name': long_name,
- 'scripts': [{
- 'file': 'payload.js',
- 'sha256': sha256(form_data['script'].encode()).digest().hex()
- }]
- }]
-
- assert uuidv4_re.match(db_contents['mapping'][0]['uuid'])
- assert db_contents['mapping'] == [{
- 'source_name': localid,
- 'source_copyright': [],
- 'type': 'mapping',
- 'identifier': localid,
- 'uuid': db_contents['mapping'][0]['uuid'],
- 'version': [1],
- 'description': form_data['description'],
- 'long_name': long_name,
- 'payloads': payloads
- }]
-
- assert_db_contents()
-
- form_data = fill_form_with_sample_data(execute_in_page, {
- 'long_name': '',
- 'description': 'bam bam bam',
- 'patterns': 'https://new.example.com/***',
- 'script': sample_files['bye.js']['contents']
- })
-
- create_but.click()
-
- for type in ('Resource', 'Mapping'):
- def override_asked(driver):
- return f"{type} 'local-someid' already exists. Override?" \
- in dialog_container.text
- WebDriverWait(driver, 10).until(override_asked)
- execute_in_page('form_ctx.dialog_ctx.yes_but.click();')
-
- assert_db_contents()
-
-@pytest.mark.ext_data({
- 'background_script': broker_js,
- 'extra_html': ExtraHTML('html/payload_create.html', {}),
- 'navigate_to': 'html/payload_create.html'
-})
-@pytest.mark.usefixtures('webextension')
-def test_payload_create_errors(driver, execute_in_page):
- """
- A test case of various error the simple payload form might show.
- """
- execute_in_page(load_script('html/payload_create.js'))
-
- create_but, dialog_container = execute_in_page(
- '''
- const form_ctx = payload_create_form();
- document.body.append(form_ctx.main_div);
- returnval([form_ctx.create_but, form_ctx.dialog_container]);
- ''')
-
- for data_override, expected_msg in [
- ({'identifier': ''}, "The 'identifier' field is required!"),
- ({'identifier': ':('}, 'Identifier may only contain '),
- ({'script': ''}, "The 'script' field is required!"),
- ({'patterns': ''}, "The 'URL patterns' field is required!"),
- ({'patterns': ':d'}, "':d' is not a valid URL pattern. See here for more details."),
- ({'patterns': '\n'.join(['http://example.com'] * 2)},
- "Pattern 'http://example.com' specified multiple times!")
- ]:
- # Attempt creating the payload
- form_data = fill_form_with_sample_data(execute_in_page, data_override)
- create_but.click()
- # Verify the error message
- assert expected_msg in dialog_container.text
-
- # Verify patterns documentation <a> link.
- if expected_msg == {'patterns': ':d'}:
- doc_link_elem = driver.find_element_by_link_text('here')
- assert doc_link.get_attribute('href') == patterns_doc_url
-
- # Verify the form was NOT cleared upon failed saving.
- execute_in_page('form_ctx.dialog_ctx.ok_but.click();')
- assert_form_contents(execute_in_page, form_data)
-
- # Add a sample item and attempt overriding it.
- fill_form_with_sample_data(execute_in_page)
- create_but.click()
- WebDriverWait(driver, 10).until(lambda _: 'Succes' in dialog_container.text)
- execute_in_page('form_ctx.dialog_ctx.ok_but.click();')
-
- # Verify that denying override leads to saving failure.
- form_data = fill_form_with_sample_data(execute_in_page)
- create_but.click()
- WebDriverWait(driver, 10).until(lambda _: 'Overri' in dialog_container.text)
- execute_in_page('form_ctx.dialog_ctx.no_but.click();')
- assert 'Failed to save payload :(' in dialog_container.text
- execute_in_page('form_ctx.dialog_ctx.ok_but.click();')
- assert_form_contents(execute_in_page, form_data)
-
- # Verify that IndexedDB errors get caught and reported as saving failures.
- execute_in_page('haketilodb.get = async () => {throw "someerror";}')
- form_data = fill_form_with_sample_data(execute_in_page, {'identifier': 'o'})
- create_but.click()
- WebDriverWait(driver, 10).until(lambda _: 'Failed' in dialog_container.text)
- execute_in_page('form_ctx.dialog_ctx.ok_but.click();')
- assert_form_contents(execute_in_page, form_data)
-
- # Verify that the loading message gets shown during IndexedDB operations.
- execute_in_page('haketilodb.get = () => new Promise(cb => null);')
- create_but.click()
- assert 'Saving payload...' in dialog_container.text
diff --git a/test/unit/test_policy_deciding.py b/test/unit/test_policy_deciding.py
deleted file mode 100644
index 75b35ac..0000000
--- a/test/unit/test_policy_deciding.py
+++ /dev/null
@@ -1,135 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - determining what to do on a given web page
-"""
-
-# This file is part of Haketilo
-#
-# Copyright (C) 2021, 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 re
-from hashlib import sha256
-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+)$')
-def parse_csp(csp):
- '''
- Parsing of CSP string into a dict. A simplified format of CSP is assumed.
- '''
- 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')
-
- return result
-
-@pytest.mark.get_page('https://gotmyowndoma.in')
-def test_decide_policy(execute_in_page):
- """
- policy.js contains code that, using a Pattern Query Tree instance and a URL,
- decides what Haketilo should do on a page opened at that URL, i.e. whether
- it should block or allow script execution and whether it should inject its
- own scripts and which ones. Test that the policy object gets constructed
- properly.
- """
- execute_in_page(load_script('common/policy.js'))
-
- policy = execute_in_page(
- '''
- returnval(decide_policy(pqt.make(), "http://unkno.wn/", true, "abcd"));
- ''')
- assert policy['allow'] == True
- for prop in ('mapping', 'payload', 'nonce', 'csp', 'error'):
- assert prop not in policy
-
- policy = execute_in_page(
- '''{
- const tree = pqt.make();
- pqt.register(tree, "http://kno.wn", "~allow", 1);
- returnval(decide_policy(tree, "http://kno.wn/", false, "abcd"));
- }''')
- assert policy['allow'] == True
- assert policy['mapping'] == '~allow'
- for prop in ('payload', 'nonce', 'csp', 'error'):
- assert prop not in policy
-
- policy = execute_in_page(
- '''
- returnval(decide_policy(pqt.make(), "http://unkno.wn/", false, "abcd"));
- '''
- )
- assert policy['allow'] == False
- 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'"
- }
-
- policy = execute_in_page(
- '''{
- const tree = pqt.make();
- pqt.register(tree, "http://kno.wn", "~allow", 0);
- returnval(decide_policy(tree, "http://kno.wn/", true, "abcd"));
- }''')
- assert policy['allow'] == False
- assert policy['mapping'] == '~allow'
- 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'"
- }
-
- policy = execute_in_page(
- '''{
- const tree = pqt.make();
- pqt.register(tree, "http://kno.wn", "m1", {identifier: "res1"});
- returnval(decide_policy(tree, "http://kno.wn/", true, "abcd"));
- }''')
- assert policy['allow'] == False
- assert policy['mapping'] == 'm1'
- assert policy['payload'] == {'identifier': 'res1'}
- assert 'error' not in policy
- 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']}'"
- }
-
- policy = execute_in_page(
- 'returnval(decide_policy(pqt.make(), "<bad_url>", true, "abcd"));'
- )
- assert policy['allow'] == False
- assert policy['error'] == {'haketilo_error_type': 'deciding_policy'}
- 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'"
- }
diff --git a/test/unit/test_policy_enforcing.py b/test/unit/test_policy_enforcing.py
deleted file mode 100644
index 4b7c173..0000000
--- a/test/unit/test_policy_enforcing.py
+++ /dev/null
@@ -1,114 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - enforcing script blocking policy from content script
-"""
-
-# 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
-import urllib.parse
-from selenium.webdriver.support.ui import WebDriverWait
-
-from ..script_loader import load_script
-from .utils import are_scripts_allowed
-
-# For simplicity, we'll use one nonce in all test cases.
-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://*;"
-}
-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}';"
-}
-
-content_script = load_script('content/policy_enforcing.js') + ''';{
-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));
-
-if (what_to_do.csp_off) {
- const orig_DOMParser = window.DOMParser;
- window.DOMParser = function() {
- const parser = new orig_DOMParser();
- this.parseFromString = () => parser.parseFromString('', 'text/html');
- }
-}
-
-enforce_blocking(what_to_do.policy);
-}'''
-
-def get(driver, page, what_to_do):
- driver.get(page + '#' + urllib.parse.quote(json.dumps(what_to_do)))
- driver.execute_script('window.before_reload = true; location.reload();')
- done = lambda _: not driver.execute_script('return window.before_reload;')
- WebDriverWait(driver, 10).until(done)
-
-@pytest.mark.ext_data({'content_script': content_script})
-@pytest.mark.usefixtures('webextension')
-# Under Mozilla we use several mechanisms of script blocking. Some serve as
-# fallbacks in case others break. CSP one of those mechanisms. Here we run the
-# test once with CSP blocking on and once without it. This allows us to verify
-# that the CSP-less blocking approaches by themselves also work. We don't do the
-# reverse (CSP on and other mechanisms off) because CSP rules added through
-# <meta> injection are not reliable enough - they do not always take effect
-# immediately and there's nothing we can do to fix it.
-@pytest.mark.parametrize('csp_off_setting', [{}, {'csp_off': True}])
-def test_policy_enforcing_html(driver, execute_in_page, csp_off_setting):
- """
- A test case of sanitizing <script>s and intrinsic javascript in pages.
- """
- # First, see if scripts run when not blocked.
- get(driver, 'https://gotmyowndoma.in/scripts_to_block_1.html', {
- 'policy': allow_policy,
- **csp_off_setting
- })
-
- for i in range(1, 3):
- driver.find_element_by_id(f'clickme{i}').click()
-
- assert set(driver.execute_script('return window.__run || [];')) == \
- {'inline', 'on', 'href', 'src', 'data'}
- assert are_scripts_allowed(driver)
-
- # Now, verify scripts don't run when blocked.
- get(driver, 'https://gotmyowndoma.in/scripts_to_block_1.html', {
- 'policy': block_policy,
- **csp_off_setting
- })
-
- for i in range(1, 3):
- driver.find_element_by_id(f'clickme{i}').click()
-
- assert set(driver.execute_script('return window.__run || [];')) == set()
- assert bool(csp_off_setting) == are_scripts_allowed(driver)
-
- # Now, verify only scripts with nonce can run when payload is injected.
- get(driver, 'https://gotmyowndoma.in/scripts_to_block_1.html', {
- 'policy': payload_policy,
- **csp_off_setting
- })
-
- for i in range(1, 3):
- driver.find_element_by_id(f'clickme{i}').click()
-
- assert set(driver.execute_script('return window.__run || [];')) == set()
- assert bool(csp_off_setting) == are_scripts_allowed(driver)
- assert are_scripts_allowed(driver, nonce)
diff --git a/test/unit/test_popup.py b/test/unit/test_popup.py
deleted file mode 100644
index 1fc262c..0000000
--- a/test/unit/test_popup.py
+++ /dev/null
@@ -1,257 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - repository querying
-"""
-
-# 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 ..extension_crafting import ExtraHTML
-from ..script_loader import load_script
-from .utils import *
-
-unprivileged_page_info = {
- 'url': 'https://example_a.com/something',
- 'allow': False
-}
-
-mapping_page_info = {
- **unprivileged_page_info,
- 'mapping': 'm1',
- 'payload': {'identifier': 'res1'}
-}
-
-mocked_page_infos = {
- 'privileged': {
- 'url': 'moz-extension://<some-id>/file.html',
- 'privileged': True
- },
- 'blocked_default': unprivileged_page_info,
- 'allowed_default': {
- **unprivileged_page_info,
- 'allow': True
- },
- 'blocked_rule': {
- **unprivileged_page_info,
- 'mapping': '~allow'
- },
- 'allowed_rule': {
- **unprivileged_page_info,
- 'allow': True,
- 'mapping': '~allow'
- },
- 'mapping': mapping_page_info,
- 'error_deciding_policy': {
- **mapping_page_info,
- 'error': {'haketilo_error_type': 'deciding_policy'}
- },
- 'error_missing': {
- **mapping_page_info,
- 'error': {'haketilo_error_type': 'missing', 'id': 'some-missing-res'}
- },
- 'error_circular': {
- **mapping_page_info,
- 'error': {'haketilo_error_type': 'circular', 'id': 'some-circular-res'}
- },
- 'error_db': {
- **mapping_page_info,
- 'error': {'haketilo_error_type': 'db'}
- },
- 'error_other': {
- **mapping_page_info,
- 'error': {'haketilo_error_type': 'other'}
- }
-}
-
-tab_mock_js = '''
-;
-const mocked_page_info = (%s)[/#mock_page_info-(.*)$/.exec(document.URL)[1]];
-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") {
- 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 {
- throw `bad sendMessage message type: '${msg[0]}'`;
- }
-}
-
-const old_tabs_query = browser.tabs.query;
-browser.tabs.query = async function(query) {
- const tabs = await old_tabs_query(query);
- tabs.forEach(t => t.url = mocked_page_info.url);
- return tabs;
-}
-''' % json.dumps(mocked_page_infos)
-
-popup_ext_data = {
- 'background_script': broker_js,
- 'extra_html': ExtraHTML(
- 'html/popup.html',
- {
- 'common/browser.js': tab_mock_js,
- 'common/indexeddb.js': '; set_repo("https://hydril.la/");'
- },
- wrap_into_htmldoc=False
- ),
- 'navigate_to': 'html/popup.html'
-}
-
-@pytest.mark.ext_data(popup_ext_data)
-@pytest.mark.usefixtures('webextension')
-@pytest.mark.parametrize('page_info_key', ['', *mocked_page_infos.keys()])
-def test_popup_display(driver, execute_in_page, page_info_key):
- """
- Test popup viewing while on a page. Test parametrized with different
- possible values of page_info object passed in message from the content
- script.
- """
- initial_url = driver.current_url
- driver.get('about:blank')
- driver.get(f'{initial_url}#mock_page_info-{page_info_key}')
-
- by_id = driver.execute_script(
- '''
- const nodes = [...document.querySelectorAll("[id]")];
- const reductor = (ob, node) => Object.assign(ob, {[node.id]: node});
- return nodes.reduce(reductor, {});
- ''')
-
- if page_info_key == '':
- error_msg = 'Page info not avaialable. Try reloading the page.'
- error_msg_shown = lambda d: by_id['loading_info'].text == error_msg
- WebDriverWait(driver, 10).until(error_msg_shown)
- return
-
- WebDriverWait(driver, 10).until(lambda d: by_id['info_form'].is_displayed())
- assert (page_info_key == 'privileged') == \
- by_id['privileged_page_info'].is_displayed()
- assert (page_info_key == 'privileged') ^ \
- by_id['unprivileged_page_info'].is_displayed()
- assert by_id['page_url'].text == mocked_page_infos[page_info_key]['url']
- assert not by_id['repo_query_container'].is_displayed()
-
- if 'allow' in page_info_key:
- assert by_id['scripts_blocked'].text.lower() == 'no'
- elif page_info_key != 'privileged':
- assert by_id['scripts_blocked'].text.lower() == 'yes'
-
- payload_text = by_id['injected_payload'].text
- if page_info_key == 'mapping':
- assert payload_text == 'res1'
- elif page_info_key == 'error_missing':
- assert payload_text == \
- "None (error: resource with id 'some-missing-res' missing from the database)"
- elif page_info_key == 'error_circular':
- assert payload_text == \
- "None (error: circular dependency of resource with id 'some-circular-res' on itself)"
- elif page_info_key == 'error_db':
- assert payload_text == \
- 'None (error: failure reading Haketilo internal database)'
- elif page_info_key == 'error_other':
- assert payload_text == \
- 'None (error: unknown failure occured)'
- elif page_info_key != 'privileged':
- assert payload_text == 'None'
-
- mapping_text = by_id['mapping_used'].text
-
- if page_info_key == 'error_deciding_policy':
- assert mapping_text == 'None (error occured when determining policy)'
- elif page_info_key == 'mapping' or page_info_key.startswith('error'):
- assert mapping_text == 'm1'
-
- if 'allowed' in page_info_key:
- assert 'None (scripts allowed by' in mapping_text
- elif 'blocked' in page_info_key:
- assert 'None (scripts blocked by' in mapping_text
-
- if 'rule' in page_info_key:
- assert 'by a rule)' in mapping_text
- elif 'default' in page_info_key:
- assert 'by default policy)' in mapping_text
-
-@pytest.mark.ext_data(popup_ext_data)
-@pytest.mark.usefixtures('webextension')
-def test_popup_repo_query(driver, execute_in_page):
- """
- Test opening and closing the repo query view in popup.
- """
- initial_url = driver.current_url
- driver.get('about:blank')
- driver.get(f'{initial_url}#mock_page_info-blocked_rule')
-
- search_but = driver.find_element_by_id("search_resources_but")
- WebDriverWait(driver, 10).until(lambda d: search_but.is_displayed())
- search_but.click()
-
- containers = dict([(name, driver.find_element_by_id(f'{name}_container'))
- for name in ('page_info', 'repo_query')])
- assert not containers['page_info'].is_displayed()
- assert containers['repo_query'].is_displayed()
- shown = lambda d: 'https://hydril.la/' in containers['repo_query'].text
- WebDriverWait(driver, 10).until(shown)
-
- # Click the "Show results" button.
- selector = '.repo_query_buttons > button:first-child'
- driver.find_element_by_css_selector(selector).click()
- shown = lambda d: 'MAPPING_A' in containers['repo_query'].text
- WebDriverWait(driver, 10).until(shown)
-
- # Click the "Cancel" button
- selector = '.repo_query_bottom_buttons > button'
- driver.find_element_by_css_selector(selector).click()
- assert containers['page_info'].is_displayed()
- assert not containers['repo_query'].is_displayed()
-
-@pytest.mark.ext_data(popup_ext_data)
-@pytest.mark.usefixtures('webextension')
-# Under Parabola's Iceweasel 75 the settings page's window opened during this
-# test is impossible to close using driver.close() - it raises an exception with
-# message 'closeTab() not supported in iceweasel'. To avoid such error during
-# test cleanup, we use the mark below to tell our driver fixture to span a
-# separate browser instance for this test.
-@pytest.mark.second_driver()
-def test_popup_settings_opening(driver, execute_in_page):
- """
- Test opening the settings page from popup through button click.
- """
- driver.find_element_by_id("settings_but").click()
-
- first_handle = driver.current_window_handle
- WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) == 2)
- new_handle = [h for h in driver.window_handles if h != first_handle][0]
-
- driver.switch_to.window(new_handle)
- driver.implicitly_wait(10)
- assert "Extension's options page for testing" in \
- driver.find_element_by_tag_name("h1").text
diff --git a/test/unit/test_repo_query.py b/test/unit/test_repo_query.py
deleted file mode 100644
index c8c4875..0000000
--- a/test/unit/test_repo_query.py
+++ /dev/null
@@ -1,274 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - repository querying
-"""
-
-# 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
-from selenium.webdriver.support.ui import WebDriverWait
-
-from ..extension_crafting import ExtraHTML
-from ..script_loader import load_script
-from .utils import *
-
-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):
- mock_cacher(execute_in_page)
-
- execute_in_page(load_script('html/repo_query.js'))
- execute_in_page(
- '''
- const repo_proms = arguments[0].map(url => haketilodb.set_repo(url));
-
- const cb_calls = [];
- const view = new RepoQueryView(0,
- () => cb_calls.push("show"),
- () => cb_calls.push("hide"));
- document.body.append(view.main_div);
- const shw = slice => [cb_calls.slice(slice || 0), view.shown];
-
- returnval(Promise.all(repo_proms));
- ''',
- repo_urls)
-
-repo_query_ext_data = {
- 'background_script': broker_js,
- 'extra_html': ExtraHTML('html/repo_query.html', {}),
- 'navigate_to': 'html/repo_query.html'
-}
-
-@pytest.mark.ext_data(repo_query_ext_data)
-@pytest.mark.usefixtures('webextension')
-def test_repo_query_normal_usage(driver, execute_in_page):
- """
- Test of using the repo query view to browse results from repository and to
- start installation.
- """
- setup_view(execute_in_page, repo_urls)
-
- assert execute_in_page('returnval(shw());') == [[], False]
-
- execute_in_page('view.show(arguments[0]);', queried_url)
-
- assert execute_in_page('returnval(shw());') == [['show'], True]
-
- def get_repo_entries(driver):
- return execute_in_page(
- f'returnval((view.repo_entries || []).map({nodes_props_code}));'
- )
-
- repo_entries = WebDriverWait(driver, 10).until(get_repo_entries)
-
- assert len(repo_urls) == len(repo_entries)
-
- for url, entry in reversed(list(zip(repo_urls, repo_entries))):
- assert url in entry['main_li'].text
-
- but_ids = ('show_results_but', 'hide_results_but')
- for but_idx in (0, 1, 0):
- assert bool(but_idx) == entry['list_container'].is_displayed()
-
- assert not entry[but_ids[1 - but_idx]].is_displayed()
-
- entry[but_ids[but_idx]].click()
-
- def get_mapping_entries(driver):
- return execute_in_page(
- f'''{{
- const result_entries = (view.repo_entries[0].result_entries || []);
- returnval(result_entries.map({nodes_props_code}));
- }}''')
-
- mapping_entries = WebDriverWait(driver, 10).until(get_mapping_entries)
-
- assert len(mapping_entries) == 3
-
- expected_names = ['MAPPING_ABCD', 'MAPPING_ABCD-DEFG-GHIJ', 'MAPPING_A']
-
- for name, entry in zip(expected_names, mapping_entries):
- assert entry['mapping_name'].text == name
- assert entry['mapping_id'].text == f'{name.lower()}-2022.5.11'
-
- containers = execute_in_page(
- '''{
- const reductor = (acc, k) => Object.assign(acc, {[k]: view[k]});
- returnval(container_ids.reduce(reductor, {}));
- }''')
-
- for id, container in containers.items():
- assert (id == 'repos_list_container') == container.is_displayed()
-
- entry['install_but'].click()
-
- for id, container in containers.items():
- assert (id == 'install_view_container') == container.is_displayed()
-
- execute_in_page('returnval(view.install_view.cancel_but);').click()
-
- for id, container in containers.items():
- assert (id == 'repos_list_container') == container.is_displayed()
-
- assert execute_in_page('returnval(shw());') == [['show'], True]
- execute_in_page('returnval(view.cancel_but);').click()
- assert execute_in_page('returnval(shw());') == [['show', 'hide'], False]
-
-@pytest.mark.ext_data(repo_query_ext_data)
-@pytest.mark.usefixtures('webextension')
-@pytest.mark.parametrize('message', [
- 'browsing_for',
- 'no_repos',
- 'failure_to_communicate',
- 'HTTP_code',
- 'invalid_JSON',
- 'newer_API_version',
- 'invalid_response_format',
- 'querying_repo',
- 'no_results'
-])
-def test_repo_query_messages(driver, execute_in_page, message):
- """
- Test of loading and error messages shown in parts of the repo query view.
- """
- def has_msg(message, elem=None):
- def has_msg_and_is_visible(dummy_driver):
- if elem:
- return elem.is_displayed() and message in elem.text
- else:
- return message in driver.page_source
- return has_msg_and_is_visible
-
- def show_and_wait_for_repo_entry():
- execute_in_page('view.show(arguments[0]);', queried_url)
- done = lambda d: execute_in_page('returnval(!!view.repo_entries);')
- WebDriverWait(driver, 10).until(done)
- execute_in_page(
- '''
- if (view.repo_entries.length > 0)
- view.repo_entries[0].show_results_but.click();
- ''')
-
- if message == 'browsing_for':
- setup_view(execute_in_page, [])
- show_and_wait_for_repo_entry()
-
- elem = execute_in_page('returnval(view.url_span.parentNode);')
- assert has_msg(f'Browsing custom resources for: {queried_url}', elem)(0)
- elif message == 'no_repos':
- setup_view(execute_in_page, [])
- show_and_wait_for_repo_entry()
-
- elem = execute_in_page('returnval(view.repos_list);')
- done = has_msg('You have no repositories configured :(', elem)
- WebDriverWait(driver, 10).until(done)
- elif message == 'failure_to_communicate':
- setup_view(execute_in_page, repo_urls)
- execute_in_page(
- 'browser.tabs.sendMessage = () => Promise.resolve({error: "sth"});'
- )
- show_and_wait_for_repo_entry()
-
- elem = execute_in_page('returnval(view.repo_entries[0].info_div);')
- done = has_msg('Failure to communicate with repository :(', elem)
- WebDriverWait(driver, 10).until(done)
- elif message == 'HTTP_code':
- setup_view(execute_in_page, repo_urls)
- execute_in_page(
- '''
- const response = {ok: false, status: 405};
- browser.tabs.sendMessage = () => Promise.resolve(response);
- ''')
- show_and_wait_for_repo_entry()
-
- elem = execute_in_page('returnval(view.repo_entries[0].info_div);')
- done = has_msg('Repository sent HTTP code 405 :(', elem)
- WebDriverWait(driver, 10).until(done)
- elif message == 'invalid_JSON':
- 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);
- ''')
- show_and_wait_for_repo_entry()
-
- elem = execute_in_page('returnval(view.repo_entries[0].info_div);')
- done = has_msg("Repository's response is not valid JSON :(", elem)
- WebDriverWait(driver, 10).until(done)
- elif message == 'newer_API_version':
- 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-3.2.1.schema.json"}
- };
- browser.tabs.sendMessage = () => Promise.resolve(response);
- ''')
- show_and_wait_for_repo_entry()
-
- elem = execute_in_page('returnval(view.repo_entries[0].info_div);')
- msg = 'Results were served using unsupported Hydrilla API version. You might need to update Haketilo.'
- WebDriverWait(driver, 10).until(has_msg(msg, elem))
- elif message == 'invalid_response_format':
- 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);
- ''')
- show_and_wait_for_repo_entry()
-
- elem = execute_in_page('returnval(view.repo_entries[0].info_div);')
- msg = 'Results were served using a nonconforming response format.'
- WebDriverWait(driver, 10).until(has_msg(msg, elem))
- elif message == 'querying_repo':
- setup_view(execute_in_page, repo_urls)
- execute_in_page(
- 'browser.tabs.sendMessage = () => new Promise(() => {});'
- )
- show_and_wait_for_repo_entry()
-
- elem = execute_in_page('returnval(view.repo_entries[0].info_div);')
- assert has_msg('Querying repository...', elem)(0)
- elif message == 'no_results':
- 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);
- ''')
- show_and_wait_for_repo_entry()
-
- elem = execute_in_page('returnval(view.repo_entries[0].info_div);')
- WebDriverWait(driver, 10).until(has_msg('No results :(', elem))
- else:
- raise Exception('made a typo in test function params?')
diff --git a/test/unit/test_repo_query_cacher.py b/test/unit/test_repo_query_cacher.py
deleted file mode 100644
index 5fbc5cd..0000000
--- a/test/unit/test_repo_query_cacher.py
+++ /dev/null
@@ -1,130 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - caching responses from remote repositories
-"""
-
-# 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
-
-def content_script():
- script = load_script('content/repo_query_cacher.js')
- return f'{script}; {tab_id_asker}; start();'
-
-def bypass_js():
- return load_script('background/CORS_bypass_server.js') + '; start();'
-
-def fetch_through_cache(driver, tab_id, url):
- return driver.execute_script(
- '''
- return browser.tabs.sendMessage(arguments[0],
- ["repo_query", arguments[1]]);
- ''',
- tab_id, url)
-
-"""
-tab_id_responder is meant to be appended to background script of a test
-extension.
-"""
-tab_id_responder = '''
-function tell_tab_id(msg, sender, respond_cb) {
- if (msg[0] === "learn_tab_id")
- respond_cb(sender.tab.id);
-}
-browser.runtime.onMessage.addListener(tell_tab_id);
-'''
-
-"""
-tab_id_asker is meant to be appended to content script of a test extension.
-"""
-tab_id_asker = '''
-browser.runtime.sendMessage(["learn_tab_id"])
- .then(tid => window.wrappedJSObject.haketilo_tab = tid);
-'''
-
-def run_content_script_in_new_window(driver, url):
- """
- Expect an extension to be loaded which had tab_id_responder and tab_id_asker
- appended to its background and content scripts, respectively.
- Open the provided url in a new tab, find its tab id and return it, with
- current window changed back to the initial one.
- """
- handle0 = driver.current_window_handle
- initial_handles = [*driver.window_handles]
- driver.execute_script('window.open(arguments[0], "_blank");', url)
- window_added = lambda d: set(d.window_handles) != set(initial_handles)
- WebDriverWait(driver, 10).until(window_added)
- new_handle = [*set(driver.window_handles).difference(initial_handles)][0]
-
- driver.switch_to.window(new_handle)
-
- get_tab_id = lambda d: d.execute_script('return window.haketilo_tab;')
- tab_id = WebDriverWait(driver, 10).until(get_tab_id)
-
- driver.switch_to.window(handle0)
- return tab_id
-
-@pytest.mark.ext_data({
- 'content_script': content_script,
- 'background_script': lambda: bypass_js() + ';' + tab_id_responder
-})
-@pytest.mark.usefixtures('webextension')
-def test_repo_query_cacher_normal_use(driver, execute_in_page):
- """
- 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 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
-
- 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
-
- 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'}
-
-@pytest.mark.ext_data({
- 'content_script': content_script,
- 'background_script': tab_id_responder
-})
-@pytest.mark.usefixtures('webextension')
-def test_repo_query_cacher_bgscript_error(driver):
- """
- Test if our cacher properly reports errors in communication with the
- background script.
- """
- 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()) == {'error'}
diff --git a/test/unit/test_settings.py b/test/unit/test_settings.py
deleted file mode 100644
index 7cdb76f..0000000
--- a/test/unit/test_settings.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - entire settings page
-"""
-
-# 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
-from .utils import *
-
-from ..extension_crafting import ExtraHTML
-from ..script_loader import load_script
-from .utils import *
-
-@pytest.mark.ext_data({
- 'background_script': broker_js,
- 'extra_html': ExtraHTML('html/settings.html', wrap_into_htmldoc=False)
-})
-@pytest.mark.usefixtures('webextension')
-def test_settings_page_tabs(driver, execute_in_page):
- """
- Test navigation throught the tabs of the settings page.
- """
- # First, put some sample data in IndexedDB.
- execute_in_page(load_script('common/indexeddb.js'))
- execute_in_page(
- '''
- initial_data = arguments[0];
- returnval(get_db().then(() => {}));
- ''',
- make_complete_sample_data())
-
- # Now navigate to settings page.
- testpage_url = driver.execute_script('return window.location.href;')
- driver.get(testpage_url.replace('testpage.html', 'html/settings.html'))
-
- names = ['blocking', 'mappings', 'resources', 'new_payload', 'repos']
- tabs = dict([(n, driver.find_element_by_id(f'{n}_tab')) for n in names])
- heads = dict([(n, driver.find_element_by_id(f'{n}_head')) for n in names])
-
- for i, tab_name in enumerate(['new_payload', *names]):
- if (i > 0):
- heads[tab_name].click()
-
- assert 'active_head' in heads[tab_name].get_attribute('class')
- assert 'active_tab' in tabs[tab_name].get_attribute('class')
- assert tabs[tab_name].is_displayed()
- for tab_name_2 in [n for n in names if n != tab_name]:
- assert 'active_head' not in heads[tab_name_2].get_attribute('class')
- assert 'active_tab' not in tabs[tab_name_2].get_attribute('class')
- assert not tabs[tab_name_2].is_displayed()
diff --git a/test/unit/test_text_entry_list.py b/test/unit/test_text_entry_list.py
deleted file mode 100644
index 3135a59..0000000
--- a/test/unit/test_text_entry_list.py
+++ /dev/null
@@ -1,387 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - list of editable entries
-"""
-
-# 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
-from selenium.webdriver.support.ui import WebDriverWait
-from selenium.webdriver.common.keys import Keys
-import inspect
-
-from ..extension_crafting import ExtraHTML
-from ..script_loader import load_script
-from .utils import *
-
-list_code_template = '(await blocking_allowing_lists(%%s))[%d]'
-mode_parameters = [
- #add_action del_action instantiate_code
- ('set_repo', 'del_repo', 'await repo_list(%s)'),
- ('set_disallowed', 'set_default_allowing', list_code_template % 0),
- ('set_disallowed', 'set_allowed', list_code_template % 0),
- ('set_allowed', 'set_default_allowing', list_code_template % 1),
- ('set_allowed', 'set_disallowed', list_code_template % 1)
-]
-
-def instantiate_list(to_return):
- instantiate_code = inspect.stack()[1].frame.f_locals['instantiate_code']
- return inspect.stack()[1].frame.f_locals['execute_in_page'](
- f'''
- let dialog_ctx = dialog.make(() => {{}}, () => {{}}), list;
- async function make_list() {{
- list = {instantiate_code % 'dialog_ctx'};
- document.body.append(list.main_div, dialog_ctx.main_div);
- return [{', '.join(to_return)}];
- }}
- returnval(make_list());
- ''')
-
-dialog_html_append = {'html/text_entry_list.html': '#INCLUDE html/dialog.html'}
-dialog_html_test_ext_data = {
- 'background_script': broker_js,
- 'extra_html': ExtraHTML('html/text_entry_list.html', dialog_html_append),
- 'navigate_to': 'html/text_entry_list.html'
-}
-
-@pytest.mark.ext_data(dialog_html_test_ext_data)
-@pytest.mark.usefixtures('webextension')
-@pytest.mark.parametrize('mode', mode_parameters)
-def test_text_entry_list_ordering(driver, execute_in_page, mode):
- """
- A test case of ordering of repo URLs or URL patterns in the list.
- """
- add_action, del_action, instantiate_code = mode
-
- execute_in_page(load_script('html/text_entry_list.js'))
-
- endings = ['hyd/', 'hydrilla/', 'Hydrilla/', 'HYDRILLA/',
- 'test/', 'test^it/', 'Test^it/', 'TEST^IT/']
-
- indexes_added = set()
-
- for iteration, to_include in enumerate([
- set([i for i in range(len(endings)) if is_prime(i)]),
- set([i for i in range(len(endings))
- if not is_prime(i) and i & 1]),
- set([i for i in range(len(endings)) if i % 3 == 0]),
- set([i for i in range(len(endings))
- if i % 3 and not i & 1 and not is_prime(i)]),
- set(range(len(endings)))
- ]):
- endings_to_include = [endings[i] for i in sorted(to_include)]
- urls = [f'https://example.com/{e}' for e in endings_to_include]
-
- def add_urls():
- execute_in_page(
- '''{
- async function add_urls(urls, add_action) {
- for (const url of urls)
- await haketilodb[add_action](url);
- }
- returnval(add_urls(...arguments));
- }''',
- urls, add_action)
-
- def wait_for_completed(wait_id):
- """
- We add an extra url to IndexedDB and wait for it to appear in the
- DOM list. Once this happes, we know other operations must have also
- finished.
- """
- url = f'https://example.org/{iteration}/{wait_id}'
- execute_in_page(
- '''
- returnval(haketilodb[arguments[1]](arguments[0]));
- ''',
- url, add_action)
- WebDriverWait(driver, 10).until(lambda _: url in list_div.text)
-
- def assert_order(indexes_present, empty_entry_expected=False):
- entries_texts = execute_in_page(
- '''
- returnval([...list.list_div.children].map(n => n.textContent));
- ''')
-
- if empty_entry_expected:
- assert 'example' not in entries_texts[0]
- entries_texts.pop(0)
-
- for i, et in zip(sorted(indexes_present), entries_texts):
- assert f'https://example.com/{endings[i]}' in et
-
- for et in entries_texts[len(indexes_present):]:
- assert 'example.org' in et
-
- add_urls()
-
- if iteration == 0:
- list_div, new_entry_but = \
- instantiate_list(['list.list_div', 'list.new_but'])
-
- indexes_added.update(to_include)
- wait_for_completed(0)
- assert_order(indexes_added)
-
- execute_in_page(
- '''{
- async function remove_urls(urls, del_action) {
- for (const url of urls)
- await haketilodb[del_action](url);
- }
- returnval(remove_urls(...arguments));
- }''',
- urls, del_action)
- wait_for_completed(1)
- assert_order(indexes_added.difference(to_include))
-
- # On the last iteration, add a new editable entry before re-additions.
- if len(to_include) == len(endings):
- new_entry_but.click()
- add_urls()
- wait_for_completed(2)
- assert_order(indexes_added, empty_entry_expected=True)
- else:
- add_urls()
-
-def active(id):
- return inspect.stack()[1].frame.f_locals['execute_in_page']\
- (f'returnval(list.active_entry.{id});')
-def existing(id, entry_nr=0):
- return inspect.stack()[1].frame.f_locals['execute_in_page'](
- '''
- returnval(list.entries_by_text.get(list.shown_texts[arguments[0]])\
- [arguments[1]]);
- ''',
- entry_nr, id)
-
-@pytest.mark.ext_data(dialog_html_test_ext_data)
-@pytest.mark.usefixtures('webextension')
-@pytest.mark.parametrize('mode', [mp for mp in mode_parameters
- if mp[1] != 'set_default_allowing'])
-def test_text_entry_list_editing(driver, execute_in_page, mode):
- """
- A test case of editing entries in repo URLs list.
- """
- add_action, _, instantiate_code = mode
-
- execute_in_page(load_script('html/text_entry_list.js'))
-
- execute_in_page(
- '''
- let original_loader = dialog.loader, last_loader_msg;
- dialog.loader = (ctx, ...msg) => {
- last_loader_msg = msg;
- return original_loader(ctx, ...msg);
- }
- ''')
- last_loader_msg = lambda: execute_in_page('returnval(last_loader_msg);')
-
- list_div, new_entry_but = \
- instantiate_list(['list.list_div', 'list.new_but'])
-
- if 'allow' in add_action:
- assert last_loader_msg() == ['Loading script blocking settings...']
- else:
- assert last_loader_msg() == ['Loading repositories...']
-
- assert execute_in_page('returnval(dialog_ctx.shown);') == False
-
- # Test adding new item. Submit via button click.
- new_entry_but.click()
- assert not active('noneditable_view').is_displayed()
- assert not active('save_but').is_displayed()
- assert active('add_but').is_displayed()
- assert active('cancel_but').is_displayed()
- active('input').send_keys('https://example.com///')
- active('add_but').click()
- WebDriverWait(driver, 10).until(lambda _: 'example.com' in list_div.text)
- assert execute_in_page('returnval(list.list_div.children.length);') == 1
- if 'disallow' in add_action:
- assert last_loader_msg() == \
- ["Blocking scripts on 'https://example.com/'..."]
- elif 'allow' in add_action:
- assert last_loader_msg() == \
- ["Allowing scripts on 'https://example.com/'..."]
- else:
- assert last_loader_msg() == \
- ["Adding repository 'https://example.com/'..."]
-
- assert not existing('editable_view').is_displayed()
- assert existing('text').is_displayed()
- assert existing('remove_but').is_displayed()
-
- # Test editing item. Submit via 'Enter' hit. Also test url pattern
- # normalization.
- existing('text').click()
- assert not active('noneditable_view').is_displayed()
- assert not active('add_but').is_displayed()
- assert active('save_but').is_displayed()
- assert active('cancel_but').is_displayed()
- assert active('input.value') == 'https://example.com/'
- active('input').send_keys(Keys.BACKSPACE * 30 + 'https://example.org//a//b'
- + Keys.ENTER)
- WebDriverWait(driver, 10).until(lambda _: 'example.org' in list_div.text)
- assert execute_in_page('returnval(list.list_div.children.length);') == 1
- if 'disallow' in add_action:
- assert last_loader_msg() == ['Rewriting script blocking rule...']
- elif 'allow' in add_action:
- assert last_loader_msg() == ['Rewriting script allowing rule...']
- else:
- assert last_loader_msg() == ['Replacing repository...']
-
- # Test entry removal.
- existing('remove_but').click()
- WebDriverWait(driver, 10).until(lambda _: 'xample.org' not in list_div.text)
- assert execute_in_page('returnval(list.list_div.children.length);') == 0
- if 'allow' in add_action:
- assert last_loader_msg() == \
- ["Setting default scripts blocking policy on 'https://example.org/a/b'..."]
- else:
- assert last_loader_msg() == ["Removing repository 'https://example.org//a//b/'..."]
-
- # The rest of this test remains the same regardless of mode. No point
- # testing the same thing multiple times.
- if 'repo' not in add_action:
- return
-
- # Test that clicking hidden buttons of item not being edited does nothing.
- new_entry_but.click()
- active('input').send_keys('https://example.foo' + Keys.ENTER)
- WebDriverWait(driver, 10).until(lambda _: 'xample.foo/' in list_div.text)
- existing('add_but.click()')
- existing('save_but.click()')
- existing('cancel_but.click()')
- assert execute_in_page('returnval(dialog_ctx.shown);') == False
- assert execute_in_page('returnval(list.list_div.children.length);') == 1
- assert not existing('editable_view').is_displayed()
-
- # Test that clicking hidden buttons of item being edited does nothing.
- existing('text').click()
- active('remove_but.click()')
- active('add_but.click()')
- assert execute_in_page('returnval(dialog_ctx.shown);') == False
- assert execute_in_page('returnval(list.list_div.children.length);') == 1
- assert not active('noneditable_view').is_displayed()
-
- # Test that creating a new entry makes the other one noneditable again.
- new_entry_but.click()
- assert existing('text').is_displayed()
-
- # Test that clicking hidden buttons of new item entry does nothing.
- active('remove_but.click()')
- active('save_but.click()')
- assert execute_in_page('returnval(dialog_ctx.shown);') == False
- assert execute_in_page('returnval(list.list_div.children.length);') == 2
- assert not active('noneditable_view').is_displayed()
-
- # Test that starting edit of another entry removes the new entry.
- existing('text').click()
- assert existing('editable_view').is_displayed()
- assert execute_in_page('returnval(list.list_div.children.length);') == 1
-
- # Test that starting edit of another entry cancels edit of the first entry.
- new_entry_but.click()
- active('input').send_keys('https://example.net' + Keys.ENTER)
- WebDriverWait(driver, 10).until(lambda _: 'example.net/' in list_div.text)
- assert execute_in_page('returnval(list.list_div.children.length);') == 2
- existing('text', 0).click()
- assert existing('editable_view', 0).is_displayed()
- assert not existing('editable_view', 1).is_displayed()
- existing('text', 1).click()
- assert not existing('editable_view', 0).is_displayed()
- assert existing('editable_view', 1).is_displayed()
-
-@pytest.mark.ext_data(dialog_html_test_ext_data)
-@pytest.mark.usefixtures('webextension')
-@pytest.mark.parametrize('mode', [mp for mp in mode_parameters
- if mp[1] != 'set_default_allowing'])
-def test_text_entry_list_errors(driver, execute_in_page, mode):
- """
- A test case of error dialogs shown by repo URL list.
- """
- add_action, _, instantiate_code = mode
-
- execute_in_page(load_script('html/text_entry_list.js'))
-
- to_return = ['list.list_div', 'list.new_but', 'dialog_ctx.main_div']
- list_div, new_entry_but, dialog_div = instantiate_list(to_return)
-
- # Prepare one entry to use later.
- new_entry_but.click()
- active('input').send_keys('https://example.com' + Keys.ENTER)
-
- # Check invalid URL errors.
- for clickable in (existing('text'), new_entry_but):
- clickable.click()
- active('input').send_keys(Keys.BACKSPACE * 30 + 'ws://example'
- + Keys.ENTER)
- execute_in_page('dialog.close(dialog_ctx);')
-
- if 'allow' in add_action:
- assert "'ws://example' is not a valid URL pattern. See here for more details." \
- in dialog_div.text
- assert patterns_doc_url == \
- driver.find_element_by_link_text('here').get_attribute('href')
- continue
- else:
- assert 'Repository URLs shoud use https:// schema.' \
- in dialog_div.text
-
- active('input').send_keys(Keys.BACKSPACE * 30 + 'https://example'
- + Keys.ENTER)
- assert 'Provided URL is not valid.' in dialog_div.text
- execute_in_page('dialog.close(dialog_ctx);')
-
- # Mock errors to force error messages to appear.
- execute_in_page(
- '''
- for (const action of [
- "set_repo", "del_repo", "set_allowed", "set_default_allowing"
- ])
- haketilodb[action] = () => {throw "reckless, limitless scope";};
- ''')
-
- # Check database error dialogs.
- def check_reported_failure(txt):
- fail = lambda _: txt in dialog_div.text
- WebDriverWait(driver, 10).until(fail)
- execute_in_page('dialog.close(dialog_ctx);')
-
- existing('text').click()
- active('input').send_keys(Keys.BACKSPACE * 30 + 'https://example.org'
- + Keys.ENTER)
- if 'disallow' in add_action:
- check_reported_failure('Failed to rewrite blocking rule :(')
- elif 'allow' in add_action:
- check_reported_failure('Failed to rewrite allowing rule :(')
- else:
- check_reported_failure('Failed to replace repository :(')
-
- active('cancel_but').click()
- existing('remove_but').click()
- if 'allow' in add_action:
- check_reported_failure("Failed to remove rule for 'https://example.com' :(")
- else:
- check_reported_failure("Failed to remove repository 'https://example.com/' :(")
-
- new_entry_but.click()
- active('input').send_keys('https://example.org' + Keys.ENTER)
- if 'disallow' in add_action:
- check_reported_failure("Failed to write blocking rule for 'https://example.org' :(")
- elif 'allow' in add_action:
- check_reported_failure("Failed to write allowing rule for 'https://example.org' :(")
- else:
- check_reported_failure("Failed to add repository 'https://example.org/' :(")
diff --git a/test/unit/test_webrequest.py b/test/unit/test_webrequest.py
deleted file mode 100644
index fb24b3d..0000000
--- a/test/unit/test_webrequest.py
+++ /dev/null
@@ -1,68 +0,0 @@
-# SPDX-License-Identifier: CC0-1.0
-
-"""
-Haketilo unit tests - modifying requests using webRequest API
-"""
-
-# This file is part of Haketilo
-#
-# Copyright (C) 2021, 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 re
-from hashlib import sha256
-import pytest
-
-from ..script_loader import load_script
-from .utils import are_scripts_allowed
-
-def webrequest_js():
- return (load_script('background/webrequest.js',
- '#IMPORT common/patterns_query_tree.js AS pqt') +
- ''';
- // Mock pattern tree.
- tree = pqt.make();
- // Mock default allow.
- default_allow = {name: "default_allow", value: true};
-
- // Rule to block scripts.
- pqt.register(tree, "https://site.with.scripts.block.ed/***",
- "~allow", 0);
-
- // Rule to allow scripts, but overridden by payload assignment.
- pqt.register(tree, "https://site.with.paylo.ad/***", "~allow", 1);
- pqt.register(tree, "https://site.with.paylo.ad/***",
- "somemapping", {identifier: "someresource"});
-
- // Mock stream_filter.
- stream_filter.apply = (details, headers, policy) => headers;
-
- // Mock secret and start webrequest operations.
- start("somesecret");
- ''')
-
-@pytest.mark.ext_data({'background_script': webrequest_js})
-@pytest.mark.usefixtures('webextension')
-def test_on_headers_received(driver, execute_in_page):
- for attempt in range(10):
- driver.get('https://site.with.scripts.block.ed/')
-
- if not are_scripts_allowed(driver):
- break
- assert attempt != 9
-
- driver.get('https://site.with.scripts.allow.ed/')
- assert are_scripts_allowed(driver)
-
- driver.get('https://site.with.paylo.ad/')
- assert not are_scripts_allowed(driver)
- source = 'somemapping:someresource:https://site.with.paylo.ad/index.html:somesecret'
- assert are_scripts_allowed(driver, sha256(source.encode()).digest().hex())
diff --git a/test/unit/utils.py b/test/unit/utils.py
deleted file mode 100644
index b27a209..0000000
--- a/test/unit/utils.py
+++ /dev/null
@@ -1,293 +0,0 @@
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-"""
-Various functions and objects that can be reused between unit tests
-"""
-
-# This file is part of Haketilo.
-#
-# Copyright (C) 2021,2022 Wojtek Kosior <koszko@koszko.org>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# 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
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <https://www.gnu.org/licenses/>.
-#
-# I, Wojtek Kosior, thereby promise not to sue for violation of this file's
-# license. Although I request that you do not make use of this code in a
-# proprietary program, I am not going to enforce this in court.
-
-from hashlib import sha256
-from selenium.webdriver.support.ui import WebDriverWait
-
-from ..script_loader import load_script
-
-patterns_doc_url = \
- 'https://hydrillabugs.koszko.org/projects/haketilo/wiki/URL_patterns'
-
-def sample_file(contents):
- return {
- 'sha256': sha256(contents.encode()).digest().hex(),
- 'contents': contents
- }
-
-def make_sample_files(names_contents):
- """
- Take a dict mapping file names to file contents. Return, as a tuple, dicts
- mapping file names to file objects (dicts) and file hash keys to file
- contents.
- """
- sample_files = dict([(name, sample_file(contents))
- for name, contents in names_contents.items()])
-
- sample_files_by_sha256 = dict([[file['sha256'], file['contents']]
- for file in sample_files.values()])
-
- return sample_files, sample_files_by_sha256
-
-sample_files, sample_files_by_sha256 = make_sample_files({
- 'report.spdx': '<!-- dummy report -->',
- 'LICENSES/somelicense.txt': 'Permission is granted...',
- 'LICENSES/CC0-1.0.txt': 'Dummy Commons...',
- 'hello.js': 'console.log("uńićódę hello!");\n',
- 'bye.js': 'console.log("bye!");\n',
- 'combined.js': 'console.log("hello!\\nbye!");\n',
- 'README.md': '# Python Frobnicator\n...'
-})
-
-def sample_file_ref(file_name, sample_files_dict=sample_files):
- """
- Return a dictionary suitable for using as file reference in resource/mapping
- definition.
- """
- return {
- 'file': file_name,
- 'sha256': sample_files_dict[file_name]['sha256']
- }
-
-def make_sample_mapping(with_files=True):
- """
- Procude a sample mapping definition that can be dumped to JSON and put into
- Haketilo's IndexedDB.
- """
- return {
- '$schema': 'https://hydrilla.koszko.org/schemas/api_mapping_description-1.schema.json',
- 'generated_by': {
- 'name': 'human',
- 'version': 'sapiens-0.8.14'
- },
- 'source_name': 'example-org-fixes-new',
- 'source_copyright': [
- sample_file_ref('report.spdx'),
- sample_file_ref('LICENSES/CC0-1.0.txt')
- ] if with_files else [],
- 'type': 'mapping',
- 'identifier': 'example-org-minimal',
- 'long_name': 'Example.org Minimal',
- 'uuid': '54d23bba-472e-42f5-9194-eaa24c0e3ee7',
- 'version': [2022, 5, 10],
- 'description': 'suckless something something',
- 'payloads': {
- 'https://example.org/a/*': {
- 'identifier': 'some-KISS-resource'
- },
- 'https://example.org/t/*': {
- 'identifier': 'another-KISS-resource'
- }
- }
- }
-
-def make_sample_resource(with_files=True):
- """
- Procude a sample resource definition that can be dumped to JSON and put into
- Haketilo's IndexedDB.
- """
- return {
- '$schema': 'https://hydrilla.koszko.org/schemas/api_resource_description-1.schema.json',
- 'generated_by': {
- 'name': 'human',
- 'version': 'sapiens-0.8.14'
- },
- 'source_name': 'hello',
- 'source_copyright': [
- sample_file_ref('report.spdx'),
- sample_file_ref('LICENSES/CC0-1.0.txt')
- ] if with_files else [],
- 'type': 'resource',
- 'identifier': 'helloapple',
- 'long_name': 'Hello Apple',
- 'uuid': 'a6754dcb-58d8-4b7a-a245-24fd7ad4cd68',
- 'version': [2021, 11, 10],
- 'revision': 1,
- 'description': 'greets an apple',
- 'dependencies': [{'identifier': 'hello-message'}],
- 'scripts': [
- sample_file_ref('hello.js'),
- sample_file_ref('bye.js')
- ] if with_files else []
- }
-
-def item_version_string(definition, include_revision=False):
- """
- Given a resource or mapping definition, read its "version" property (and
- also "revision" if applicable) and produce a corresponding version string.
- """
- ver = '.'.join([str(num) for num in definition['version']])
- revision = definition.get('revision') if include_revision else None
- return f'{ver}-{revision}' if revision is not None else ver
-
-def sample_data_dict(items):
- """
- Some IndexedDB functions expect saved items to be provided in a nested dict
- that makes them queryable by identifier by version. This function converts
- items list to such dict.
- """
- return dict([(it['identifier'], {item_version_string(it): it})
- for it in items])
-
-def make_complete_sample_data():
- """
- Craft a JSON data item with 1 sample resource and 1 sample mapping that can
- be used to populate IndexedDB.
- """
- return {
- 'resource': sample_data_dict([make_sample_resource()]),
- 'mapping': sample_data_dict([make_sample_mapping()]),
- 'file': {
- 'sha256': sample_files_by_sha256
- }
- }
-
-def clear_indexeddb(execute_in_page):
- """
- Remove Haketilo data from IndexedDB. If variables from common/indexeddb.js
- are in the global scope, this function will handle closing the opened
- database instance (if any). Otherwise, the caller is responsible for making
- sure the database being deleted is not opened anywhere.
- """
- execute_in_page(
- '''{
- async function delete_db() {
- if (typeof db !== "undefined" && db) {
- db.close();
- db = null;
- }
- let resolve, reject;
- const result = new Promise((...cbs) => [resolve, reject] = cbs);
- const request = indexedDB.deleteDatabase("haketilo");
- [request.onsuccess, request.onerror] = [resolve, reject];
- await result;
- }
-
- returnval(delete_db());
- }'''
- )
-
-def get_db_contents(execute_in_page):
- """
- Retrieve all IndexedDB contents. It is expected that either variables from
- common/indexeddb.js are in the global scope or common/indexeddb.js is
- imported as haketilodb.
- """
- return execute_in_page(
- '''{
- async function get_database_contents()
- {
- const db_getter =
- typeof haketilodb === "undefined" ? get_db : haketilodb.get;
- const db = await db_getter();
-
- const transaction = db.transaction(db.objectStoreNames);
- const result = {};
-
- for (const store_name of db.objectStoreNames) {
- const req = transaction.objectStore(store_name).getAll();
- await new Promise(cb => req.onsuccess = cb);
- result[store_name] = req.result;
- }
-
- return result;
- }
- returnval(get_database_contents());
- }''')
-
-def is_prime(n):
- return n > 1 and all([n % i != 0 for i in range(2, n)])
-
-broker_js = lambda: load_script('background/broadcast_broker.js') + ';start();'
-
-def are_scripts_allowed(driver, nonce=None):
- return driver.execute_script(
- '''
- document.haketilo_scripts_allowed = false;
- const script = document.createElement("script");
- script.innerHTML = "document.haketilo_scripts_allowed = true;";
- if (arguments[0])
- script.setAttribute("nonce", arguments[0]);
- document.head.append(script);
- return document.haketilo_scripts_allowed;
- ''',
- nonce)
-
-def mock_broadcast(execute_in_page):
- """
- Make all broadcast operations no-ops (broadcast must be imported).
- """
- 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;
- }
-
- browser.tabs.sendMessage = new_sendMessage;
- }''')
-
-"""
-Convenience snippet of code to retrieve a copy of given object with only those
-properties present which are DOM nodes. This makes it possible to easily access
-DOM nodes stored in a javascript object that also happens to contain some
-other properties that make it impossible to return from a Selenium script.
-"""
-nodes_props_code = '''\
-(obj => {
- const result = {};
- for (const [key, value] of Object.entries(obj)) {
- if (value instanceof Node)
- result[key] = value;
- }
- return result;
-})'''