Project

General

Profile

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

haketilo / test / unit / utils.py @ 1c65dd5c

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 sample_file(contents):
37
    return {
38
        'sha256': sha256(contents.encode()).digest().hex(),
39
        'contents': contents
40
    }
41

    
42
def make_sample_files(names_contents):
43
    """
44
    Take a dict mapping file names to file contents. Return, as a tuple, dicts
45
    mapping file names to file objects (dicts) and file hash keys to file
46
    contents.
47
    """
48
    sample_files = dict([(name, sample_file(contents))
49
                         for name, contents in names_contents.items()])
50

    
51
    sample_files_by_sha256 = dict([[file['sha256'], file['contents']]
52
                                   for file in sample_files.values()])
53

    
54
    return sample_files, sample_files_by_sha256
55

    
56
sample_files, sample_files_by_sha256 = make_sample_files({
57
    'report.spdx':              '<!-- dummy report -->',
58
    'LICENSES/somelicense.txt': 'Permission is granted...',
59
    'LICENSES/CC0-1.0.txt':     'Dummy Commons...',
60
    'hello.js':                 'console.log("uńićódę hello!");\n',
61
    'bye.js':                   'console.log("bye!");\n',
62
    'combined.js':              'console.log("hello!\\nbye!");\n',
63
    'README.md':                '# Python Frobnicator\n...'
64
})
65

    
66
def sample_file_ref(file_name, sample_files_dict=sample_files):
67
    """
68
    Return a dictionary suitable for using as file reference in resource/mapping
69
    definition.
70
    """
71
    return {
72
        'file': file_name,
73
        'sha256': sample_files_dict[file_name]['sha256']
74
    }
75

    
76
def make_sample_mapping(with_files=True):
77
    """
78
    Procude a sample mapping definition that can be dumped to JSON and put into
79
    Haketilo's IndexedDB.
80
    """
81
    return {
82
        'source_name': 'example-org-fixes-new',
83
        'source_copyright': [
84
            sample_file_ref('report.spdx'),
85
            sample_file_ref('LICENSES/CC0-1.0.txt')
86
        ] if with_files else [],
87
        'type': 'mapping',
88
        'identifier': 'example-org-minimal',
89
        'long_name': 'Example.org Minimal',
90
        'uuid': '54d23bba-472e-42f5-9194-eaa24c0e3ee7',
91
        'version': [2022, 5, 10],
92
        'description': 'suckless something something',
93
        'payloads': {
94
            'https://example.org/a/*': {
95
                'identifier': 'some-KISS-resource'
96
            },
97
            'https://example.org/t/*': {
98
                'identifier':  'another-KISS-resource'
99
            }
100
        }
101
    }
102

    
103
def make_sample_resource(with_files=True):
104
    """
105
    Procude a sample resource definition that can be dumped to JSON and put into
106
    Haketilo's IndexedDB.
107
    """
108
    return {
109
        'source_name': 'hello',
110
        'source_copyright': [
111
            sample_file_ref('report.spdx'),
112
            sample_file_ref('LICENSES/CC0-1.0.txt')
113
        ] if with_files else [],
114
        'type': 'resource',
115
        'identifier': 'helloapple',
116
        'long_name': 'Hello Apple',
117
        'uuid': 'a6754dcb-58d8-4b7a-a245-24fd7ad4cd68',
118
        'version': [2021, 11, 10],
119
        'revision': 1,
120
        'description': 'greets an apple',
121
        'dependencies': ['hello-message'],
122
        'scripts': [
123
            sample_file_ref('hello.js'),
124
            sample_file_ref('bye.js')
125
        ] if with_files else []
126
    }
127

    
128
def item_version_string(definition, include_revision=False):
129
    """
130
    Given a resource or mapping definition, read its "version" property (and
131
    also "revision" if applicable) and produce a corresponding version string.
132
    """
133
    ver = '.'.join([str(num) for num in definition['version']])
134
    revision = definition.get('revision') if include_revision else None
135
    return f'{ver}-{revision}' if revision is not None else ver
136

    
137
def sample_data_dict(items):
138
    """
139
    Some IndexedDB functions expect saved items to be provided in a nested dict
140
    that makes them queryable by identifier by version. This function converts
141
    items list to such dict.
142
    """
143
    return dict([(it['identifier'], {item_version_string(it): it})
144
                 for it in items])
145

    
146
def make_complete_sample_data():
147
    """
148
    Craft a JSON data item with 1 sample resource and 1 sample mapping that can
149
    be used to populate IndexedDB.
150
    """
151
    return {
152
        'resources': sample_data_dict([make_sample_resource()]),
153
        'mappings': sample_data_dict([make_sample_mapping()]),
154
        'file': {
155
            'sha256': sample_files_by_sha256
156
        }
157
    }
158

    
159
def clear_indexeddb(execute_in_page):
160
    """
161
    Remove Haketilo data from IndexedDB. If variables from common/indexeddb.js
162
    are in the global scope, this function will handle closing the opened
163
    database instance (if any). Otherwise, the caller is responsible for making
164
    sure the database being deleted is not opened anywhere.
165
    """
166
    execute_in_page(
167
        '''{
168
        async function delete_db() {
169
            if (typeof db !== "undefined" && db) {
170
                db.close();
171
                db = null;
172
            }
173
            let resolve, reject;
174
            const result = new Promise((...cbs) => [resolve, reject] = cbs);
175
            const request = indexedDB.deleteDatabase("haketilo");
176
            [request.onsuccess, request.onerror] = [resolve, reject];
177
            await result;
178
        }
179

    
180
        returnval(delete_db());
181
        }'''
182
    )
183

    
184
def get_db_contents(execute_in_page):
185
    """
186
    Retrieve all IndexedDB contents. It is expected that either variables from
187
    common/indexeddb.js are in the global scope or common/indexeddb.js is
188
    imported as haketilodb.
189
    """
190
    return execute_in_page(
191
        '''{
192
        async function get_database_contents()
193
        {
194
            const db_getter =
195
                  typeof haketilodb === "undefined" ? get_db : haketilodb.get;
196
            const db = await db_getter();
197

    
198
            const transaction = db.transaction(db.objectStoreNames);
199
            const result = {};
200

    
201
            for (const store_name of db.objectStoreNames) {
202
                const req = transaction.objectStore(store_name).getAll();
203
                await new Promise(cb => req.onsuccess = cb);
204
                result[store_name] = req.result;
205
            }
206

    
207
            return result;
208
        }
209
        returnval(get_database_contents());
210
        }''')
211

    
212
def is_prime(n):
213
    return n > 1 and all([n % i != 0 for i in range(2, n)])
214

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

    
217
def are_scripts_allowed(driver, nonce=None):
218
        return driver.execute_script(
219
            '''
220
            document.haketilo_scripts_allowed = false;
221
            const script = document.createElement("script");
222
            script.innerHTML = "document.haketilo_scripts_allowed = true;";
223
            if (arguments[0])
224
                script.setAttribute("nonce", arguments[0]);
225
            document.head.append(script);
226
            return document.haketilo_scripts_allowed;
227
            ''',
228
            nonce)
229

    
230
def mock_broadcast(execute_in_page):
231
    """
232
    Make all broadcast operations no-ops (broadcast must be imported).
233
    """
234
    execute_in_page(
235
        'Object.keys(broadcast).forEach(k => broadcast[k] = () => {});'
236
    )
237

    
238
def mock_cacher(execute_in_page):
239
    """
240
    Some parts of code depend on content/repo_query_cacher.js and
241
    background/CORS_bypass_server.js running in their appropriate contexts. This
242
    function modifies the relevant browser.runtime.sendMessage function to
243
    perform fetch(), bypassing the cacher.
244
    """
245
    execute_in_page(
246
        '''{
247
        const old_sendMessage = browser.tabs.sendMessage, old_fetch = fetch;
248
        async function new_sendMessage(tab_id, msg) {
249
            if (msg[0] !== "repo_query")
250
                return old_sendMessage(tab_id, msg);
251

    
252
            /* Use snapshotted fetch(), allow other test code to override it. */
253
            const response = await old_fetch(msg[1]);
254
            if (!response)
255
                return {error: "Something happened :o"};
256

    
257
            const result = {ok: response.ok, status: response.status};
258
            try {
259
                result.json = await response.json();
260
            } catch(e) {
261
                result.error_json = "" + e;
262
            }
263
            return result;
264
        }
265

    
266
        browser.tabs.sendMessage = new_sendMessage;
267
        }''')
268

    
269
"""
270
Convenience snippet of code to retrieve a copy of given object with only those
271
properties present which are DOM nodes. This makes it possible to easily access
272
DOM nodes stored in a javascript object that also happens to contain some
273
other properties that make it impossible to return from a Selenium script.
274
"""
275
nodes_props_code = '''\
276
(obj => {
277
    const result = {};
278
    for (const [key, value] of Object.entries(obj)) {
279
        if (value instanceof Node)
280
            result[key] = value;
281
    }
282
    return result;
283
})'''
(25-25/25)