Project

General

Profile

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

haketilo / test / haketilo_test / unit / utils.py @ aec5c9ae

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
        '$schema': 'https://hydrilla.koszko.org/schemas/api_mapping_description-1.schema.json',
83
        'generated_by': {
84
            'name': 'human',
85
            'version': 'sapiens-0.8.14'
86
        },
87
        'source_name': 'example-org-fixes-new',
88
        'source_copyright': [
89
            sample_file_ref('report.spdx'),
90
            sample_file_ref('LICENSES/CC0-1.0.txt')
91
        ] if with_files else [],
92
        'type': 'mapping',
93
        'identifier': 'example-org-minimal',
94
        'long_name': 'Example.org Minimal',
95
        'uuid': '54d23bba-472e-42f5-9194-eaa24c0e3ee7',
96
        'version': [2022, 5, 10],
97
        'description': 'suckless something something',
98
        'payloads': {
99
            'https://example.org/a/*': {
100
                'identifier': 'some-KISS-resource'
101
            },
102
            'https://example.org/t/*': {
103
                'identifier':  'another-KISS-resource'
104
            }
105
        }
106
    }
107

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

    
138
def item_version_string(definition, include_revision=False):
139
    """
140
    Given a resource or mapping definition, read its "version" property (and
141
    also "revision" if applicable) and produce a corresponding version string.
142
    """
143
    ver = '.'.join([str(num) for num in definition['version']])
144
    revision = definition.get('revision') if include_revision else None
145
    return f'{ver}-{revision}' if revision is not None else ver
146

    
147
def sample_data_dict(items):
148
    """
149
    Some IndexedDB functions expect saved items to be provided in a nested dict
150
    that makes them queryable by identifier by version. This function converts
151
    items list to such dict.
152
    """
153
    return dict([(it['identifier'], {item_version_string(it): it})
154
                 for it in items])
155

    
156
def make_complete_sample_data():
157
    """
158
    Craft a JSON data item with 1 sample resource and 1 sample mapping that can
159
    be used to populate IndexedDB.
160
    """
161
    return {
162
        'resource': sample_data_dict([make_sample_resource()]),
163
        'mapping': sample_data_dict([make_sample_mapping()]),
164
        'file': {
165
            'sha256': sample_files_by_sha256
166
        }
167
    }
168

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

    
190
        returnval(delete_db());
191
        }'''
192
    )
193

    
194
def get_db_contents(execute_in_page):
195
    """
196
    Retrieve all IndexedDB contents. It is expected that either variables from
197
    common/indexeddb.js are in the global scope or common/indexeddb.js is
198
    imported as haketilodb.
199
    """
200
    return execute_in_page(
201
        '''{
202
        async function get_database_contents()
203
        {
204
            const db_getter =
205
                  typeof haketilodb === "undefined" ? get_db : haketilodb.get;
206
            const db = await db_getter();
207

    
208
            const transaction = db.transaction(db.objectStoreNames);
209
            const result = {};
210

    
211
            for (const store_name of db.objectStoreNames) {
212
                const req = transaction.objectStore(store_name).getAll();
213
                await new Promise(cb => req.onsuccess = cb);
214
                result[store_name] = req.result;
215
            }
216

    
217
            return result;
218
        }
219
        returnval(get_database_contents());
220
        }''')
221

    
222
def is_prime(n):
223
    return n > 1 and all([n % i != 0 for i in range(2, n)])
224

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

    
227
def are_scripts_allowed(driver, nonce=None):
228
        return driver.execute_script(
229
            '''
230
            document.haketilo_scripts_allowed = false;
231
            document.haketilo_eval_allowed = false;
232
            const html_ns = "http://www.w3.org/1999/xhtml";
233
            const script = document.createElementNS(html_ns, "script");
234
            script.innerHTML = `
235
                document.haketilo_scripts_allowed = true;
236
                eval('document.haketilo_eval_allowed = true;');
237
            `;
238
            if (arguments[0])
239
                script.setAttribute("nonce", arguments[0]);
240
            (document.head || document.documentElement).append(script);
241

    
242
            if (document.haketilo_scripts_allowed !=
243
                document.haketilo_eval_allowed)
244
                throw "scripts allowed but eval blocked";
245

    
246
            return document.haketilo_scripts_allowed;
247
            ''',
248
            nonce)
249

    
250
def mock_broadcast(execute_in_page):
251
    """
252
    Make all broadcast operations no-ops (broadcast must be imported).
253
    """
254
    execute_in_page(
255
        'Object.keys(broadcast).forEach(k => broadcast[k] = () => {});'
256
    )
257

    
258
"""
259
Some parts of code depend on content/repo_query_cacher.js and
260
background/CORS_bypass_server.js running in their appropriate contexts. This
261
snippet modifies the relevant browser.runtime.sendMessage function to perform
262
fetch(), bypassing the cacher.
263
"""
264
mock_cacher_code = '''{
265
const uint8_to_hex =
266
    array => [...array].map(b => ("0" + b.toString(16)).slice(-2)).join("");
267

    
268
const old_sendMessage = browser.tabs.sendMessage;
269
window.mock_cacher_fetch = fetch;
270
browser.tabs.sendMessage = async function(tab_id, msg) {
271
    if (msg[0] !== "repo_query")
272
        return old_sendMessage(tab_id, msg);
273

    
274
    /*
275
     * Use snapshotted fetch() under the name window.mock_cacher_fetch,
276
     * allow other test code to override it.
277
     */
278
    try {
279
        const response = await window.mock_cacher_fetch(msg[1]);
280
        const buf = await response.arrayBuffer();
281
        return {
282
            status:     response.status,
283
            statusText: response.statusText,
284
            headers:    [...response.headers.entries()],
285
            body:       uint8_to_hex(new Uint8Array(buf))
286
        }
287
    } catch(e) {
288
        return {error: {name: e.name, message: e.message}};
289
    }
290
}
291
}'''
292

    
293
"""
294
Convenience snippet of code to retrieve a copy of given object with only those
295
properties present which are DOM nodes. This makes it possible to easily access
296
DOM nodes stored in a javascript object that also happens to contain some
297
other properties that make it impossible to return from a Selenium script.
298
"""
299
nodes_props_code = '''\
300
(obj => {
301
    const result = {};
302
    for (const [key, value] of Object.entries(obj)) {
303
        if (value instanceof Node)
304
            result[key] = value;
305
    }
306
    return result;
307
})'''
(26-26/26)