Project

General

Profile

Download (8.47 KB) Statistics
| Branch: | Tag: | Revision:

haketilo / test / unit / utils.py @ 7218849a

1
# SPDX-License-Identifier: GPL-3.0-or-later
2

    
3
"""
4
Various functions and objects that can be reused between unit tests
5
"""
6

    
7
# This file is part of Haketilo.
8
#
9
# Copyright (C) 2021,2022 Wojtek Kosior <koszko@koszko.org>
10
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the GNU General Public License as published by
13
# the Free Software Foundation, either version 3 of the License, or
14
# (at your option) any later version.
15
#
16
# This program is distributed in the hope that it will be useful,
17
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
# GNU General Public License for more details.
20
#
21
# You should have received a copy of the GNU General Public License
22
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
23
#
24
# I, Wojtek Kosior, thereby promise not to sue for violation of this file's
25
# license. Although I request that you do not make use of this code in a
26
# proprietary program, I am not going to enforce this in court.
27

    
28
from hashlib import sha256
29
from selenium.webdriver.support.ui import WebDriverWait
30

    
31
from ..script_loader import load_script
32

    
33
patterns_doc_url = \
34
    'https://hydrillabugs.koszko.org/projects/haketilo/wiki/URL_patterns'
35

    
36
def make_hash_key(file_contents):
37
    return f'sha256-{sha256(file_contents.encode()).digest().hex()}'
38

    
39
def sample_file(contents):
40
    return {
41
        'hash_key': make_hash_key(contents),
42
        'contents': contents
43
    }
44

    
45
sample_files = {
46
    'report.spdx':              sample_file('<!-- dummy report -->'),
47
    'LICENSES/somelicense.txt': sample_file('Permission is granted...'),
48
    'LICENSES/CC0-1.0.txt':     sample_file('Dummy Commons...'),
49
    'hello.js':                 sample_file('console.log("uńićódę hello!");\n'),
50
    'bye.js':                   sample_file('console.log("bye!");\n'),
51
    'combined.js':              sample_file('console.log("hello!\\nbye!");\n'),
52
    'README.md':                sample_file('# Python Frobnicator\n...')
53
}
54

    
55
sample_files_by_hash = dict([[file['hash_key'], file['contents']]
56
                             for file in sample_files.values()])
57

    
58
def sample_file_ref(file_name):
59
    return {'file': file_name, 'hash_key': sample_files[file_name]['hash_key']}
60

    
61
def make_sample_mapping():
62
    return {
63
        'source_name': 'example-org-fixes-new',
64
        'source_copyright': [
65
            sample_file_ref('report.spdx'),
66
            sample_file_ref('LICENSES/CC0-1.0.txt')
67
        ],
68
        'type': 'mapping',
69
        'identifier': 'example-org-minimal',
70
        'long_name': 'Example.org Minimal',
71
        'uuid': '54d23bba-472e-42f5-9194-eaa24c0e3ee7',
72
        'version': [2022, 5, 10],
73
        'description': 'suckless something something',
74
        'payloads': {
75
            'https://example.org/a/*': {
76
                'identifier': 'some-KISS-resource'
77
            },
78
            'https://example.org/t/*': {
79
                'identifier':  'another-KISS-resource'
80
            }
81
        }
82
    }
83

    
84
def make_sample_resource():
85
    return {
86
        'source_name': 'hello',
87
        'source_copyright': [
88
            sample_file_ref('report.spdx'),
89
            sample_file_ref('LICENSES/CC0-1.0.txt')
90
        ],
91
        'type': 'resource',
92
        'identifier': 'helloapple',
93
        'long_name': 'Hello Apple',
94
        'uuid': 'a6754dcb-58d8-4b7a-a245-24fd7ad4cd68',
95
        'version': [2021, 11, 10],
96
        'revision': 1,
97
        'description': 'greets an apple',
98
        'dependencies': ['hello-message'],
99
        'scripts': [
100
            sample_file_ref('hello.js'),
101
            sample_file_ref('bye.js')
102
        ]
103
    }
104

    
105
def item_version_string(definition, include_revision=False):
106
    """
107
    Given a resource or mapping definition, read its "version" property (and
108
    also "revision" if applicable) and produce a corresponding version string.
109
    """
110
    ver = '.'.join([str(num) for num in definition['version']])
111
    revision = definition.get('revision') if include_revision else None
112
    return f'{ver}-{revision}' if revision is not None else ver
113

    
114
def sample_data_dict(items):
115
    """
116
    Some indexeddb functions expect saved items to be provided in a nested dict
117
    that makes them queryable by identifier by version. This function converts
118
    items list to such dict.
119
    """
120
    return dict([(it['identifier'], {item_version_string(it): it})
121
                 for it in items])
122

    
123
def make_complete_sample_data():
124
    """
125
    Craft a JSON data item with 1 sample resource and 1 sample mapping that can
126
    be used to populate IndexedDB.
127
    """
128
    return {
129
        'resources': sample_data_dict([make_sample_resource()]),
130
        'mappings': sample_data_dict([make_sample_mapping()]),
131
        'files': sample_files_by_hash
132
    }
133

    
134
def clear_indexeddb(execute_in_page):
135
    """
136
    Remove Haketilo data from IndexedDB. If variables from common/indexeddb.js
137
    are in the global scope, this function will handle closing the opened
138
    database instance (if any). Otherwise, the caller is responsible for making
139
    sure the database being deleted is not opened anywhere.
140
    """
141
    execute_in_page(
142
        '''{
143
        async function delete_db() {
144
            if (typeof db !== "undefined" && db) {
145
                db.close();
146
                db = null;
147
            }
148
            let resolve, reject;
149
            const result = new Promise((...cbs) => [resolve, reject] = cbs);
150
            const request = indexedDB.deleteDatabase("haketilo");
151
            [request.onsuccess, request.onerror] = [resolve, reject];
152
            await result;
153
        }
154

    
155
        returnval(delete_db());
156
        }'''
157
    )
158

    
159
def get_db_contents(execute_in_page):
160
    """
161
    Retrieve all IndexedDB contents. It is expected that either variables from
162
    common/indexeddb.js are in the global scope or common/indexeddb.js is
163
    imported as haketilodb.
164
    """
165
    return execute_in_page(
166
        '''{
167
        async function get_database_contents()
168
        {
169
            const db_getter =
170
                  typeof haketilodb === "undefined" ? get_db : haketilodb.get;
171
            const db = await db_getter();
172

    
173
            const transaction = db.transaction(db.objectStoreNames);
174
            const result = {};
175

    
176
            for (const store_name of db.objectStoreNames) {
177
                const req = transaction.objectStore(store_name).getAll();
178
                await new Promise(cb => req.onsuccess = cb);
179
                result[store_name] = req.result;
180
            }
181

    
182
            return result;
183
        }
184
        returnval(get_database_contents());
185
        }''')
186

    
187
def is_prime(n):
188
    return n > 1 and all([n % i != 0 for i in range(2, n)])
189

    
190
broker_js = lambda: load_script('background/broadcast_broker.js') + ';start();'
191

    
192
def are_scripts_allowed(driver, nonce=None):
193
        return driver.execute_script(
194
            '''
195
            document.haketilo_scripts_allowed = false;
196
            const script = document.createElement("script");
197
            script.innerHTML = "document.haketilo_scripts_allowed = true;";
198
            if (arguments[0])
199
                script.setAttribute("nonce", arguments[0]);
200
            document.head.append(script);
201
            return document.haketilo_scripts_allowed;
202
            ''',
203
            nonce)
204

    
205
"""
206
tab_id_responder is meant to be appended to background script of a test
207
extension.
208
"""
209
tab_id_responder = '''
210
function tell_tab_id(msg, sender, respond_cb) {
211
    if (msg[0] === "learn_tab_id")
212
        respond_cb(sender.tab.id);
213
}
214
browser.runtime.onMessage.addListener(tell_tab_id);
215
'''
216

    
217
"""
218
tab_id_asker is meant to be appended to content script of a test extension.
219
"""
220
tab_id_asker = '''
221
browser.runtime.sendMessage(["learn_tab_id"])
222
    .then(tid => window.wrappedJSObject.haketilo_tab = tid);
223
'''
224

    
225
def run_content_script_in_new_window(driver, url):
226
    """
227
    Expect an extension to be loaded which had tab_id_responder and tab_id_asker
228
    appended to its background and content scripts, respectively.
229
    Open the provided url in a new tab, find its tab id and return it, with
230
    current window changed back to the initial one.
231
    """
232
    initial_handle = driver.current_window_handle
233
    handles = driver.window_handles
234
    driver.execute_script('window.open(arguments[0], "_blank");', url)
235
    WebDriverWait(driver, 10).until(lambda d: d.window_handles is not handles)
236
    new_handle = [h for h in driver.window_handles if h not in handles][0]
237

    
238
    driver.switch_to.window(new_handle)
239

    
240
    get_tab_id = lambda d: d.execute_script('return window.haketilo_tab;')
241
    tab_id = WebDriverWait(driver, 10).until(get_tab_id)
242

    
243
    driver.switch_to.window(initial_handle)
244
    return tab_id
(22-22/22)