From fd9f2fc4783cc606734e61116185c032a63d54a0 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Wed, 16 Feb 2022 22:01:38 +0100 Subject: fix out-of-source builds --- test/haketilo_test/unit/utils.py | 293 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 293 insertions(+) create mode 100644 test/haketilo_test/unit/utils.py (limited to 'test/haketilo_test/unit/utils.py') 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 +# +# 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 . +# +# 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': '', + '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; +})''' -- cgit v1.2.3