Project

General

Profile

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

haketilo / test / unit / utils.py @ b75a5717

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
sample_files = {
46
    'report.spdx':              sample_file('<!-- dummy report -->'),
47
    'LICENSES/somelicense.txt': sample_file('Permission is granted...'),
48
    'LICENSES/CC0-1.0.txt':     sample_file('Dummy Commons...'),
49
    'hello.js':                 sample_file('console.log("uńićódę hello!");\n'),
50
    'bye.js':                   sample_file('console.log("bye!");\n'),
51
    'combined.js':              sample_file('console.log("hello!\\nbye!");\n'),
52
    'README.md':                sample_file('# Python Frobnicator\n...')
53
}
54

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

    
58
def sample_file_ref(file_name):
59
    return {'file': file_name, 'hash_key': sample_files[file_name]['hash_key']}
60

    
61
def make_sample_mapping():
62
    return {
63
        'source_name': 'example-org-fixes-new',
64
        'source_copyright': [
65
            sample_file_ref('report.spdx'),
66
            sample_file_ref('LICENSES/CC0-1.0.txt')
67
        ],
68
        'type': 'mapping',
69
        'identifier': 'example-org-minimal',
70
        'long_name': 'Example.org Minimal',
71
        'uuid': '54d23bba-472e-42f5-9194-eaa24c0e3ee7',
72
        'version': [2022, 5, 10],
73
        'description': 'suckless something something',
74
        'payloads': {
75
            'https://example.org/a/*': {
76
                'identifier': 'some-KISS-resource'
77
            },
78
            'https://example.org/t/*': {
79
                'identifier':  'another-KISS-resource'
80
            }
81
        }
82
    }
83

    
84
def make_sample_resource():
85
    return {
86
        'source_name': 'hello',
87
        'source_copyright': [
88
            sample_file_ref('report.spdx'),
89
            sample_file_ref('LICENSES/CC0-1.0.txt')
90
        ],
91
        'type': 'resource',
92
        'identifier': 'helloapple',
93
        'long_name': 'Hello Apple',
94
        'uuid': 'a6754dcb-58d8-4b7a-a245-24fd7ad4cd68',
95
        'version': [2021, 11, 10],
96
        'revision': 1,
97
        'description': 'greets an apple',
98
        'dependencies': ['hello-message'],
99
        'scripts': [
100
            sample_file_ref('hello.js'),
101
            sample_file_ref('bye.js')
102
        ]
103
    }
104

    
105
def item_version_string(definition, include_revision=False):
106
    """
107
    Given a resource or mapping definition, read its "version" property (and
108
    also "revision" if applicable) and produce a corresponding version string.
109
    """
110
    ver = '.'.join([str(num) for num in definition['version']])
111
    revision = definition.get('revision') if include_revision else None
112
    return f'{ver}-{revision}' if revision is not None else ver
113

    
114
def sample_data_dict(items):
115
    """
116
    Some indexeddb functions expect saved items to be provided in a nested dict
117
    that makes them queryable by identifier by version. This function converts
118
    items list to such dict.
119
    """
120
    return dict([(it['identifier'], {item_version_string(it): it})
121
                 for it in items])
122

    
123
def make_complete_sample_data():
124
    """
125
    Craft a JSON data item with 1 sample resource and 1 sample mapping that can
126
    be used to populate IndexedDB.
127
    """
128
    return {
129
        'resources': sample_data_dict([make_sample_resource()]),
130
        'mappings': sample_data_dict([make_sample_mapping()]),
131
        'files': sample_files_by_hash
132
    }
133

    
134
def clear_indexeddb(execute_in_page):
135
    """
136
    Remove Haketilo data from IndexedDB. If variables from common/indexeddb.js
137
    are in the global scope, this function will handle closing the opened
138
    database instance (if any). Otherwise, the caller is responsible for making
139
    sure the database being deleted is not opened anywhere.
140
    """
141
    execute_in_page(
142
        '''{
143
        async function delete_db() {
144
            if (typeof db !== "undefined" && db) {
145
                db.close();
146
                db = null;
147
            }
148
            let resolve, reject;
149
            const result = new Promise((...cbs) => [resolve, reject] = cbs);
150
            const request = indexedDB.deleteDatabase("haketilo");
151
            [request.onsuccess, request.onerror] = [resolve, reject];
152
            await result;
153
        }
154

    
155
        returnval(delete_db());
156
        }'''
157
    )
158

    
159
def get_db_contents(execute_in_page):
160
    """
161
    Retrieve all IndexedDB contents. It is expected that either variables from
162
    common/indexeddb.js are in the global scope or common/indexeddb.js is
163
    imported as haketilodb.
164
    """
165
    return execute_in_page(
166
        '''{
167
        async function get_database_contents()
168
        {
169
            const db_getter =
170
                  typeof haketilodb === "undefined" ? get_db : haketilodb.get;
171
            const db = await db_getter();
172

    
173
            const transaction = db.transaction(db.objectStoreNames);
174
            const result = {};
175

    
176
            for (const store_name of db.objectStoreNames) {
177
                const req = transaction.objectStore(store_name).getAll();
178
                await new Promise(cb => req.onsuccess = cb);
179
                result[store_name] = req.result;
180
            }
181

    
182
            return result;
183
        }
184
        returnval(get_database_contents());
185
        }''')
186

    
187
def is_prime(n):
188
    return n > 1 and all([n % i != 0 for i in range(2, n)])
189

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

    
192
def are_scripts_allowed(driver, nonce=None):
193
        return driver.execute_script(
194
            '''
195
            document.haketilo_scripts_allowed = false;
196
            const script = document.createElement("script");
197
            script.innerHTML = "document.haketilo_scripts_allowed = true;";
198
            if (arguments[0])
199
                script.setAttribute("nonce", arguments[0]);
200
            document.head.append(script);
201
            return document.haketilo_scripts_allowed;
202
            ''',
203
            nonce)
204

    
205
def mock_cacher(execute_in_page):
206
    """
207
    Some parts of code depend on content/repo_query_cacher.js and
208
    background/CORS_bypass_server.js running in their appropriate contexts. This
209
    function modifies the relevant browser.runtime.sendMessage function to
210
    perform fetch(), bypassing the cacher.
211
    """
212
    execute_in_page(
213
        '''{
214
        const old_sendMessage = browser.tabs.sendMessage, old_fetch = fetch;
215
        async function new_sendMessage(tab_id, msg) {
216
            if (msg[0] !== "repo_query")
217
                return old_sendMessage(tab_id, msg);
218

    
219
            /* Use snapshotted fetch(), allow other test code to override it. */
220
            const response = await old_fetch(msg[1]);
221
            if (!response)
222
                return {error: "Something happened :o"};
223

    
224
            const result = {ok: response.ok, status: response.status};
225
            try {
226
                result.json = await response.json();
227
            } catch(e) {
228
                result.error_json = "" + e;
229
            }
230
            return result;
231
        }
232

    
233
        browser.tabs.sendMessage = new_sendMessage;
234
        }''')
235

    
236
"""
237
Convenience snippet of code to retrieve a copy of given object with only those
238
properties present which are DOM nodes. This makes it possible to easily access
239
DOM nodes stored in a javascript object that also happens to contain some
240
other properties that make it impossible to return from a Selenium script.
241
"""
242
nodes_props_code = '''\
243
(obj => {
244
    const result = {};
245
    for (const [key, value] of Object.entries(obj)) {
246
        if (value instanceof Node)
247
            result[key] = value;
248
    }
249
    return result;
250
})'''
(23-23/23)