Project

General

Profile

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

haketilo / test / unit / utils.py @ 5c58b3d6

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
def make_sample_files(names_contents):
46
    """
47
    Take a dict mapping file names to file contents. Return, as a tuple, dicts
48
    mapping file names to file objects (dicts) and file hash keys to file
49
    contents.
50
    """
51
    sample_files = dict([(name, sample_file(contents))
52
                         for name, contents in names_contents.items()])
53

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

    
57
    return sample_files, sample_files_by_hash
58

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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