aboutsummaryrefslogtreecommitdiff
path: root/test/haketilo_test/unit/utils.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/haketilo_test/unit/utils.py')
-rw-r--r--test/haketilo_test/unit/utils.py293
1 files changed, 293 insertions, 0 deletions
diff --git a/test/haketilo_test/unit/utils.py b/test/haketilo_test/unit/utils.py
new file mode 100644
index 0000000..b27a209
--- /dev/null
+++ b/test/haketilo_test/unit/utils.py
@@ -0,0 +1,293 @@
+# 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;
+})'''