Project

General

Profile

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

haketilo / test / unit / test_indexeddb.py @ 4c6a2323

1 3a90084e Wojtek Kosior
# SPDX-License-Identifier: CC0-1.0
2
3
"""
4
Haketilo unit tests - IndexedDB access
5
"""
6
7
# This file is part of Haketilo
8
#
9 19304cd1 Wojtek Kosior
# Copyright (C) 2021,2022 Wojtek Kosior <koszko@koszko.org>
10 3a90084e Wojtek Kosior
#
11
# This program is free software: you can redistribute it and/or modify
12
# it under the terms of the CC0 1.0 Universal License as published by
13
# the Creative Commons Corporation.
14
#
15
# This program is distributed in the hope that it will be useful,
16
# but WITHOUT ANY WARRANTY; without even the implied warranty of
17
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
# CC0 1.0 Universal License for more details.
19
20
import pytest
21 b7378a99 Wojtek Kosior
import json
22
from selenium.webdriver.common.by import By
23
from selenium.webdriver.support.ui import WebDriverWait
24
from selenium.webdriver.support import expected_conditions as EC
25
from selenium.common.exceptions import WebDriverException
26 3a90084e Wojtek Kosior
27
from ..script_loader import load_script
28 19304cd1 Wojtek Kosior
from .utils import *
29 3a90084e Wojtek Kosior
30 b7378a99 Wojtek Kosior
# Sample resource definitions. They'd normally contain more fields but here we
31
# use simplified versions.
32
33
def make_sample_resource():
34
    return {
35
        'source_copyright': [
36 448820a1 Wojtek Kosior
            sample_file_ref('report.spdx'),
37
            sample_file_ref('LICENSES/somelicense.txt')
38 b7378a99 Wojtek Kosior
        ],
39
        'type': 'resource',
40
        'identifier': 'helloapple',
41 448820a1 Wojtek Kosior
        'scripts': [sample_file_ref('hello.js'), sample_file_ref('bye.js')]
42 b7378a99 Wojtek Kosior
    }
43
44
def make_sample_mapping():
45
    return {
46
        'source_copyright': [
47 448820a1 Wojtek Kosior
            sample_file_ref('report.spdx'),
48
            sample_file_ref('README.md')
49 b7378a99 Wojtek Kosior
        ],
50
        'type': 'mapping',
51
        'identifier': 'helloapple'
52
    }
53
54 702eefd2 Wojtek Kosior
@pytest.mark.get_page('https://gotmyowndoma.in')
55
def test_haketilodb_item_modifications(driver, execute_in_page):
56
    """
57
    indexeddb.js facilitates operating on Haketilo's internal database.
58
    Verify database operations on mappings/resources work properly.
59
    """
60 e7d11c7c Wojtek Kosior
    execute_in_page(load_script('common/indexeddb.js'))
61 702eefd2 Wojtek Kosior
    mock_broadcast(execute_in_page)
62
63
    # Start with no database.
64
    clear_indexeddb(execute_in_page)
65 3a90084e Wojtek Kosior
66 b7378a99 Wojtek Kosior
    sample_item = make_sample_resource()
67
    sample_item['source_copyright'][0]['extra_prop'] = True
68 3a90084e Wojtek Kosior
69 702eefd2 Wojtek Kosior
    execute_in_page(
70 3a90084e Wojtek Kosior
        '''{
71 7218849a Wojtek Kosior
        const promise = start_items_transaction(["resource"], arguments[1])
72 1e4ce148 Wojtek Kosior
            .then(ctx => save_item(arguments[0], ctx).then(() => ctx))
73 702eefd2 Wojtek Kosior
            .then(finalize_transaction);
74 1e4ce148 Wojtek Kosior
        returnval(promise);
75 3a90084e Wojtek Kosior
        }''',
76 1e4ce148 Wojtek Kosior
        sample_item, sample_files_by_hash)
77 702eefd2 Wojtek Kosior
78
    database_contents = get_db_contents(execute_in_page)
79
80 3a90084e Wojtek Kosior
    assert len(database_contents['files']) == 4
81 1e4ce148 Wojtek Kosior
    assert all([sample_files_by_hash[file['hash_key']] == file['contents']
82 3a90084e Wojtek Kosior
                for file in database_contents['files']])
83
    assert all([len(file) == 2 for file in database_contents['files']])
84
85
    assert len(database_contents['file_uses']) == 4
86
    assert all([uses['uses'] == 1 for uses in database_contents['file_uses']])
87 1e4ce148 Wojtek Kosior
    assert set([uses['hash_key'] for uses in database_contents['file_uses']]) \
88
        == set([file['hash_key'] for file in database_contents['files']])
89 3a90084e Wojtek Kosior
90 7218849a Wojtek Kosior
    assert database_contents['mapping'] == []
91
    assert database_contents['resource'] == [sample_item]
92 1e4ce148 Wojtek Kosior
93
    # See if trying to add an item without providing all its files ends in an
94
    # exception and aborts the transaction as it should.
95 448820a1 Wojtek Kosior
    sample_item['scripts'].append(sample_file_ref('combined.js'))
96 1e4ce148 Wojtek Kosior
    incomplete_files = {**sample_files_by_hash}
97
    incomplete_files.pop(sample_files['combined.js']['hash_key'])
98 702eefd2 Wojtek Kosior
    exception = execute_in_page(
99 1e4ce148 Wojtek Kosior
        '''{
100 702eefd2 Wojtek Kosior
        const args = arguments;
101
        async function try_add_item()
102
        {
103 1e4ce148 Wojtek Kosior
            const context =
104 7218849a Wojtek Kosior
                await start_items_transaction(["resource"], args[1]);
105 1e4ce148 Wojtek Kosior
            try {
106 702eefd2 Wojtek Kosior
                await save_item(args[0], context);
107
                await finalize_transaction(context);
108
                return;
109 1e4ce148 Wojtek Kosior
            } catch(e) {
110 702eefd2 Wojtek Kosior
                return e;
111 1e4ce148 Wojtek Kosior
            }
112 702eefd2 Wojtek Kosior
        }
113
        returnval(try_add_item());
114 1e4ce148 Wojtek Kosior
        }''',
115
        sample_item, incomplete_files)
116
117 702eefd2 Wojtek Kosior
    previous_database_contents = database_contents
118
    database_contents = get_db_contents(execute_in_page)
119
120
    assert 'file not present' in exception
121 1e4ce148 Wojtek Kosior
    for key, val in database_contents.items():
122
        keyfun = lambda item: item.get('hash_key') or item['identifier']
123 702eefd2 Wojtek Kosior
        assert sorted(previous_database_contents[key], key=keyfun) \
124
            == sorted(val,                             key=keyfun)
125 1e4ce148 Wojtek Kosior
126
    # See if adding another item that partially uses first's files works OK.
127 b7378a99 Wojtek Kosior
    sample_item = make_sample_mapping()
128 1e4ce148 Wojtek Kosior
    database_contents = execute_in_page(
129
        '''{
130 7218849a Wojtek Kosior
        const promise = start_items_transaction(["mapping"], arguments[1])
131 1e4ce148 Wojtek Kosior
            .then(ctx => save_item(arguments[0], ctx).then(() => ctx))
132 702eefd2 Wojtek Kosior
            .then(finalize_transaction);
133 1e4ce148 Wojtek Kosior
        returnval(promise);
134
        }''',
135
        sample_item, sample_files_by_hash)
136
137 702eefd2 Wojtek Kosior
    database_contents = get_db_contents(execute_in_page)
138
139 1e4ce148 Wojtek Kosior
    names = ['README.md', 'report.spdx', 'LICENSES/somelicense.txt', 'hello.js',
140
             'bye.js']
141
    sample_files_list = [sample_files[name] for name in names]
142
    uses_list = [1, 2, 1, 1, 1]
143
144
    uses = dict([(uses['hash_key'], uses['uses'])
145
                 for uses in database_contents['file_uses']])
146
    assert uses  == dict([(file['hash_key'], nr)
147
                          for file, nr in zip(sample_files_list, uses_list)])
148
149
    files = dict([(file['hash_key'], file['contents'])
150
                  for file in database_contents['files']])
151
    assert files == dict([(file['hash_key'], file['contents'])
152
                          for file in sample_files_list])
153
154 7218849a Wojtek Kosior
    del database_contents['resource'][0]['source_copyright'][0]['extra_prop']
155
    assert database_contents['resource'] == [make_sample_resource()]
156
    assert database_contents['mapping']  == [sample_item]
157 1e4ce148 Wojtek Kosior
158
    # Try removing the items to get an empty database again.
159
    results = [None, None]
160
    for i, item_type in enumerate(['resource', 'mapping']):
161 702eefd2 Wojtek Kosior
         execute_in_page(
162 1e4ce148 Wojtek Kosior
            f'''{{
163 b590eaa2 Wojtek Kosior
            const remover = remove_{item_type};
164 1e4ce148 Wojtek Kosior
            const promise =
165 7218849a Wojtek Kosior
                start_items_transaction(["{item_type}"], {{}})
166 1e4ce148 Wojtek Kosior
                .then(ctx => remover('helloapple', ctx).then(() => ctx))
167 702eefd2 Wojtek Kosior
                .then(finalize_transaction);
168 1e4ce148 Wojtek Kosior
            returnval(promise);
169
            }}''')
170
171 702eefd2 Wojtek Kosior
         results[i] = get_db_contents(execute_in_page)
172
173 1e4ce148 Wojtek Kosior
    names = ['README.md', 'report.spdx']
174
    sample_files_list = [sample_files[name] for name in names]
175
    uses_list = [1, 1]
176
177
    uses = dict([(uses['hash_key'], uses['uses'])
178
                 for uses in results[0]['file_uses']])
179
    assert uses  == dict([(file['hash_key'], 1) for file in sample_files_list])
180
181
    files = dict([(file['hash_key'], file['contents'])
182
                  for file in results[0]['files']])
183
    assert files == dict([(file['hash_key'], file['contents'])
184
                          for file in sample_files_list])
185
186 7218849a Wojtek Kosior
    assert results[0]['resource'] == []
187
    assert results[0]['mapping'] == [sample_item]
188 1e4ce148 Wojtek Kosior
189
    assert results[1] == dict([(key, []) for key in  results[0].keys()])
190 b7378a99 Wojtek Kosior
191
    # Try initializing an empty database with sample initial data object.
192
    sample_resource = make_sample_resource()
193
    sample_mapping = make_sample_mapping()
194
    initial_data = {
195
        'resources': {
196
            'helloapple': {
197
                '1.12':   sample_resource,
198
                '0.9':    'something_that_should_get_ignored',
199
                '1':      'something_that_should_get_ignored',
200
                '1.1':    'something_that_should_get_ignored',
201
                '1.11.1': 'something_that_should_get_ignored',
202
            }
203
        },
204
        'mappings': {
205
            'helloapple': {
206
                '0.1.1': sample_mapping
207
            }
208
        },
209
        'files': sample_files_by_hash
210
    }
211 702eefd2 Wojtek Kosior
212
    clear_indexeddb(execute_in_page)
213
    execute_in_page('initial_data = arguments[0];', initial_data)
214
    database_contents = get_db_contents(execute_in_page)
215
216 7218849a Wojtek Kosior
    assert database_contents['resource'] == [sample_resource]
217
    assert database_contents['mapping']  == [sample_mapping]
218 b7378a99 Wojtek Kosior
219 702eefd2 Wojtek Kosior
@pytest.mark.get_page('https://gotmyowndoma.in')
220
def test_haketilodb_settings(driver, execute_in_page):
221
    """
222
    indexeddb.js facilitates operating on Haketilo's internal database.
223 0feb9db2 Wojtek Kosior
    Verify assigning/retrieving values of simple "settings" item works properly.
224 702eefd2 Wojtek Kosior
    """
225 e7d11c7c Wojtek Kosior
    execute_in_page(load_script('common/indexeddb.js'))
226 702eefd2 Wojtek Kosior
    mock_broadcast(execute_in_page)
227
228
    # Start with no database.
229
    clear_indexeddb(execute_in_page)
230
231
    assert get_db_contents(execute_in_page)['settings'] == []
232
233
    assert execute_in_page('returnval(get_setting("option15"));') == None
234
235
    execute_in_page('returnval(set_setting("option15", "disable"));')
236
    assert execute_in_page('returnval(get_setting("option15"));') == 'disable'
237
238
    execute_in_page('returnval(set_setting("option15", "enable"));')
239
    assert execute_in_page('returnval(get_setting("option15"));') == 'enable'
240
241 0feb9db2 Wojtek Kosior
@pytest.mark.get_page('https://gotmyowndoma.in')
242
def test_haketilodb_allowing(driver, execute_in_page):
243
    """
244
    indexeddb.js facilitates operating on Haketilo's internal database.
245
    Verify changing the "blocking" configuration for a URL works properly.
246
    """
247 e7d11c7c Wojtek Kosior
    execute_in_page(load_script('common/indexeddb.js'))
248 0feb9db2 Wojtek Kosior
    mock_broadcast(execute_in_page)
249
250
    # Start with no database.
251
    clear_indexeddb(execute_in_page)
252
253
    assert get_db_contents(execute_in_page)['blocking'] == []
254
255
    def run_with_sample_url(expr):
256
        return execute_in_page(f'returnval({expr});', 'https://example.com/**')
257
258
    assert None == run_with_sample_url('get_allowing(arguments[0])')
259
260
    run_with_sample_url('set_disallowed(arguments[0])')
261
    assert False == run_with_sample_url('get_allowing(arguments[0])')
262
263
    run_with_sample_url('set_allowed(arguments[0])')
264
    assert True == run_with_sample_url('get_allowing(arguments[0])')
265
266
    run_with_sample_url('set_default_allowing(arguments[0])')
267
    assert None == run_with_sample_url('get_allowing(arguments[0])')
268
269
@pytest.mark.get_page('https://gotmyowndoma.in')
270
def test_haketilodb_repos(driver, execute_in_page):
271
    """
272
    indexeddb.js facilitates operating on Haketilo's internal database.
273
    Verify operations on repositories list work properly.
274
    """
275 e7d11c7c Wojtek Kosior
    execute_in_page(load_script('common/indexeddb.js'))
276 0feb9db2 Wojtek Kosior
    mock_broadcast(execute_in_page)
277
278
    # Start with no database.
279
    clear_indexeddb(execute_in_page)
280
281
    assert get_db_contents(execute_in_page)['repos'] == []
282
283
    sample_urls = ['https://hdrlla.example.com/', 'https://hdrlla.example.org']
284
285
    assert [] == execute_in_page('returnval(get_repos());')
286
287
    execute_in_page('returnval(set_repo(arguments[0]));', sample_urls[0])
288
    assert [sample_urls[0]] == execute_in_page('returnval(get_repos());')
289
290
    execute_in_page('returnval(set_repo(arguments[0]));', sample_urls[1])
291
    assert set(sample_urls) == set(execute_in_page('returnval(get_repos());'))
292
293
    execute_in_page('returnval(del_repo(arguments[0]));', sample_urls[0])
294
    assert [sample_urls[1]] == execute_in_page('returnval(get_repos());')
295
296 b7378a99 Wojtek Kosior
test_page_html = '''
297
<!DOCTYPE html>
298
<script src="/testpage.js"></script>
299 0feb9db2 Wojtek Kosior
<body>
300
</body>
301 b7378a99 Wojtek Kosior
'''
302
303
@pytest.mark.ext_data({
304
    'background_script': broker_js,
305
    'test_page':         test_page_html,
306
    'extra_files': {
307 e7d11c7c Wojtek Kosior
        'testpage.js':   lambda: load_script('common/indexeddb.js')
308 b7378a99 Wojtek Kosior
    }
309
})
310
@pytest.mark.usefixtures('webextension')
311
def test_haketilodb_track(driver, execute_in_page, wait_elem_text):
312
    """
313
    Verify IndexedDB object change notifications are correctly broadcasted
314
    through extension's background script and allow for object store contents
315
    to be tracked in any execution context.
316
    """
317 0feb9db2 Wojtek Kosior
    # Let's open the same extension's test page in a second window. Window 1
318
    # will be used to make changes to IndexedDB and window 0 to "track" those
319 b7378a99 Wojtek Kosior
    # changes.
320
    driver.execute_script('window.open(window.location.href, "_blank");')
321 4c6a2323 Wojtek Kosior
    WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) == 2)
322 b7378a99 Wojtek Kosior
    windows = [*driver.window_handles]
323
324 0feb9db2 Wojtek Kosior
    # Create elements that will have tracked data inserted under them.
325
    driver.switch_to.window(windows[0])
326
    execute_in_page('''
327
    for (const store_name of trackable) {
328
        const h2 = document.createElement("h2");
329
        h2.innerText = store_name;
330
        document.body.append(h2);
331
332
        const ul = document.createElement("ul");
333
        ul.id = store_name;
334
        document.body.append(ul);
335
    }
336
    ''')
337
338 b7378a99 Wojtek Kosior
    # Mock initial_data.
339
    sample_resource = make_sample_resource()
340
    sample_mapping = make_sample_mapping()
341
    initial_data = {
342
        'resources': {
343
            'helloapple': {
344
                '1.0': sample_resource
345
            }
346
        },
347
        'mappings': {
348
            'helloapple': {
349
                '0.1.1': sample_mapping
350
            }
351
        },
352
        'files': sample_files_by_hash
353
    }
354 0feb9db2 Wojtek Kosior
    driver.switch_to.window(windows[1])
355
    execute_in_page('initial_data = arguments[0];', initial_data)
356
    execute_in_page('returnval(set_setting("option15", "123"));')
357
    execute_in_page('returnval(set_repo("https://hydril.la"));')
358
    execute_in_page('returnval(set_disallowed("file:///*"));')
359 702eefd2 Wojtek Kosior
360
    # See if track.*() functions properly return the already-existing items.
361 0feb9db2 Wojtek Kosior
    driver.switch_to.window(windows[0])
362 b7378a99 Wojtek Kosior
    execute_in_page(
363
        '''
364
        function update_item(store_name, change)
365
        {
366 702eefd2 Wojtek Kosior
            const elem_id = `${store_name}_${change.key}`;
367 b7378a99 Wojtek Kosior
            let elem = document.getElementById(elem_id);
368
            elem = elem || document.createElement("li");
369
            elem.id = elem_id;
370
            elem.innerText = JSON.stringify(change.new_val);
371
            document.getElementById(store_name).append(elem);
372
            if (change.new_val === undefined)
373
                elem.remove();
374
        }
375
376
        let resource_tracking, resource_items, mapping_tracking, mapping_items;
377
378 702eefd2 Wojtek Kosior
        async function start_reporting()
379 b7378a99 Wojtek Kosior
        {
380 0feb9db2 Wojtek Kosior
            const props = new Map(stores.map(([sn, opt]) => [sn, opt.keyPath]));
381
            for (const store_name of trackable) {
382 702eefd2 Wojtek Kosior
                [tracking, items] =
383
                    await track[store_name](ch => update_item(store_name, ch));
384 0feb9db2 Wojtek Kosior
                const prop = props.get(store_name);
385 702eefd2 Wojtek Kosior
                for (const item of items)
386
                    update_item(store_name, {key: item[prop], new_val: item});
387
            }
388 b7378a99 Wojtek Kosior
        }
389
390 702eefd2 Wojtek Kosior
        returnval(start_reporting());
391 b7378a99 Wojtek Kosior
        ''')
392
393 0feb9db2 Wojtek Kosior
    item_counts = execute_in_page(
394
        '''{
395 b7378a99 Wojtek Kosior
        const childcount = id => document.getElementById(id).childElementCount;
396 0feb9db2 Wojtek Kosior
        returnval(trackable.map(childcount));
397
        }''')
398
    assert item_counts == [1 for _ in item_counts]
399
    for elem_id, json_value in [
400 7218849a Wojtek Kosior
            ('resource_helloapple', sample_resource),
401
            ('mapping_helloapple', sample_mapping),
402 0feb9db2 Wojtek Kosior
            ('settings_option15', {'name': 'option15', 'value': '123'}),
403
            ('repos_https://hydril.la', {'url': 'https://hydril.la'}),
404
            ('blocking_file:///*', {'pattern': 'file:///*', 'allow': False})
405
    ]:
406
        assert json.loads(driver.find_element_by_id(elem_id).text) == json_value
407 b7378a99 Wojtek Kosior
408
    # See if item additions get tracked properly.
409
    driver.switch_to.window(windows[1])
410
    sample_resource2 = make_sample_resource()
411
    sample_resource2['identifier'] = 'helloapple-copy'
412
    sample_mapping2 = make_sample_mapping()
413
    sample_mapping2['identifier'] = 'helloapple-copy'
414
    sample_data = {
415
        'resources': {
416
            'helloapple-copy': {
417
                '1.0': sample_resource2
418
            }
419
        },
420
        'mappings': {
421
            'helloapple-copy': {
422
                '0.1.1': sample_mapping2
423
            }
424
        },
425
        'files': sample_files_by_hash
426
    }
427 b590eaa2 Wojtek Kosior
    execute_in_page('returnval(save_items(arguments[0]));', sample_data)
428 702eefd2 Wojtek Kosior
    execute_in_page('returnval(set_setting("option22", "abc"));')
429 0feb9db2 Wojtek Kosior
    execute_in_page('returnval(set_repo("https://hydril2.la"));')
430
    execute_in_page('returnval(set_allowed("ftp://a.bc/"));')
431 b7378a99 Wojtek Kosior
432
    driver.switch_to.window(windows[0])
433
    driver.implicitly_wait(10)
434 0feb9db2 Wojtek Kosior
    for elem_id, json_value in [
435 7218849a Wojtek Kosior
            ('resource_helloapple-copy', sample_resource2),
436
            ('mapping_helloapple-copy', sample_mapping2),
437 0feb9db2 Wojtek Kosior
            ('settings_option22', {'name': 'option22', 'value': 'abc'}),
438
            ('repos_https://hydril2.la', {'url': 'https://hydril2.la'}),
439
            ('blocking_ftp://a.bc/', {'pattern': 'ftp://a.bc/', 'allow': True})
440
    ]:
441
        assert json.loads(driver.find_element_by_id(elem_id).text) == json_value
442 b7378a99 Wojtek Kosior
    driver.implicitly_wait(0)
443
444 0feb9db2 Wojtek Kosior
    # See if item deletions/modifications get tracked properly.
445 b7378a99 Wojtek Kosior
    driver.switch_to.window(windows[1])
446
    execute_in_page(
447
        '''{
448 0feb9db2 Wojtek Kosior
        async function change_remove_items()
449 b7378a99 Wojtek Kosior
        {
450 7218849a Wojtek Kosior
            const store_names = ["resource", "mapping"];
451 b590eaa2 Wojtek Kosior
            const ctx = await start_items_transaction(store_names, {});
452
            await remove_resource("helloapple", ctx);
453
            await remove_mapping("helloapple-copy", ctx);
454 702eefd2 Wojtek Kosior
            await finalize_transaction(ctx);
455
            await set_setting("option22", null);
456 0feb9db2 Wojtek Kosior
            await del_repo("https://hydril.la");
457
            await set_default_allowing("file:///*");
458
            await set_disallowed("ftp://a.bc/");
459 b7378a99 Wojtek Kosior
        }
460 0feb9db2 Wojtek Kosior
        returnval(change_remove_items());
461 b7378a99 Wojtek Kosior
        }''')
462
463 7218849a Wojtek Kosior
    removed_ids = ['mapping_helloapple-copy', 'resource_helloapple',
464 0feb9db2 Wojtek Kosior
                   'repos_https://hydril.la', 'blocking_file:///*']
465
    def condition_items_absent_and_changed(driver):
466 b7378a99 Wojtek Kosior
        for id in removed_ids:
467
            try:
468
                driver.find_element_by_id(id)
469
                return False
470
            except WebDriverException:
471
                pass
472 0feb9db2 Wojtek Kosior
473 702eefd2 Wojtek Kosior
        option_text = driver.find_element_by_id('settings_option22').text
474 0feb9db2 Wojtek Kosior
        blocking_text = driver.find_element_by_id('blocking_ftp://a.bc/').text
475
        return (json.loads(option_text)['value'] == None and
476
                json.loads(blocking_text)['allow'] == False)
477 b7378a99 Wojtek Kosior
478
    driver.switch_to.window(windows[0])
479 0feb9db2 Wojtek Kosior
    WebDriverWait(driver, 10).until(condition_items_absent_and_changed)