Project

General

Profile

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

haketilo / test / haketilo_test / unit / utils.py @ 9bee4afa

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
            const html_ns = "http://www.w3.org/1999/xhtml";
232
            const script = document.createElementNS(html_ns, "script");
233
            script.innerHTML = "document.haketilo_scripts_allowed = true;";
234
            if (arguments[0])
235
                script.setAttribute("nonce", arguments[0]);
236
            (document.head || document.documentElement).append(script);
237
            return document.haketilo_scripts_allowed;
238
            ''',
239
            nonce)
240

    
241
def mock_broadcast(execute_in_page):
242
    """
243
    Make all broadcast operations no-ops (broadcast must be imported).
244
    """
245
    execute_in_page(
246
        'Object.keys(broadcast).forEach(k => broadcast[k] = () => {});'
247
    )
248

    
249
"""
250
Some parts of code depend on content/repo_query_cacher.js and
251
background/CORS_bypass_server.js running in their appropriate contexts. This
252
snippet modifies the relevant browser.runtime.sendMessage function to perform
253
fetch(), bypassing the cacher.
254
"""
255
mock_cacher_code = '''{
256
const uint8_to_hex =
257
    array => [...array].map(b => ("0" + b.toString(16)).slice(-2)).join("");
258

    
259
const old_sendMessage = browser.tabs.sendMessage;
260
window.mock_cacher_fetch = fetch;
261
browser.tabs.sendMessage = async function(tab_id, msg) {
262
    if (msg[0] !== "repo_query")
263
        return old_sendMessage(tab_id, msg);
264

    
265
    /*
266
     * Use snapshotted fetch() under the name window.mock_cacher_fetch,
267
     * allow other test code to override it.
268
     */
269
    try {
270
        const response = await window.mock_cacher_fetch(msg[1]);
271
        const buf = await response.arrayBuffer();
272
        return {
273
            status:     response.status,
274
            statusText: response.statusText,
275
            headers:    [...response.headers.entries()],
276
            body:       uint8_to_hex(new Uint8Array(buf))
277
        }
278
    } catch(e) {
279
        return {error: {name: e.name, message: e.message}};
280
    }
281
}
282
}'''
283

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