Project

General

Profile

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

haketilo / test / unit / utils.py @ 72553a2d

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 script = document.createElement("script");
232
            script.innerHTML = "document.haketilo_scripts_allowed = true;";
233
            if (arguments[0])
234
                script.setAttribute("nonce", arguments[0]);
235
            document.head.append(script);
236
            return document.haketilo_scripts_allowed;
237
            ''',
238
            nonce)
239

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

    
248
def mock_cacher(execute_in_page):
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
    function modifies the relevant browser.runtime.sendMessage function to
253
    perform fetch(), bypassing the cacher.
254
    """
255
    execute_in_page(
256
        '''{
257
        const old_sendMessage = browser.tabs.sendMessage, old_fetch = fetch;
258
        async function new_sendMessage(tab_id, msg) {
259
            if (msg[0] !== "repo_query")
260
                return old_sendMessage(tab_id, msg);
261

    
262
            /* Use snapshotted fetch(), allow other test code to override it. */
263
            const response = await old_fetch(msg[1]);
264
            if (!response)
265
                return {error: "Something happened :o"};
266

    
267
            const result = {ok: response.ok, status: response.status};
268
            try {
269
                result.json = await response.json();
270
            } catch(e) {
271
                result.error_json = "" + e;
272
            }
273
            return result;
274
        }
275

    
276
        browser.tabs.sendMessage = new_sendMessage;
277
        }''')
278

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