Project

General

Profile

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

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

1
# 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
# 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 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
import json
22
from selenium.webdriver.common.by import By
23
from selenium.webdriver.support.ui import WebDriverWait
24
from selenium.common.exceptions import WebDriverException
25

    
26
from ..script_loader import load_script
27
from .utils import *
28

    
29
# Sample resource definitions. They'd normally contain more fields but here we
30
# use simplified versions.
31

    
32
def make_sample_resource():
33
    return {
34
        'source_copyright': [
35
            sample_file_ref('report.spdx'),
36
            sample_file_ref('LICENSES/somelicense.txt')
37
        ],
38
        'type': 'resource',
39
        'identifier': 'helloapple',
40
        'scripts': [sample_file_ref('hello.js'), sample_file_ref('bye.js')]
41
    }
42

    
43
def make_sample_mapping():
44
    return {
45
        'source_copyright': [
46
            sample_file_ref('report.spdx'),
47
            sample_file_ref('README.md')
48
        ],
49
        'type': 'mapping',
50
        'identifier': 'helloapple'
51
    }
52

    
53
@pytest.mark.get_page('https://gotmyowndoma.in')
54
def test_haketilodb_update_1_0_1(driver, execute_in_page):
55
    """
56
    indexeddb.js modifies data when updating to database version 1.0.1. Verify
57
    the update procedure works properly.
58
    """
59
    execute_in_page(load_script('common/indexeddb.js'))
60
    execute_in_page('db_version_nr = version_nr([1, 0, 0]);')
61
    mock_broadcast(execute_in_page)
62

    
63
    # Start with no database.
64
    clear_indexeddb(execute_in_page)
65

    
66
    v1_url = 'https://hydrilla.koszko.org/api_v1/'
67
    v2_url = 'https://hydrilla.koszko.org/api_v2/'
68
    urls_v1 = {'https://sample.url/abc/', v1_url}
69
    urls_v2 = {'https://sample.url/abc/', v2_url}
70

    
71
    for url in urls_v1:
72
        execute_in_page('returnval(set_repo(arguments[0]));', url)
73

    
74
    assert urls_v1 == set(execute_in_page('returnval(get_repos());'))
75

    
76
    # Verify that url gets updated to v2 upon database update.
77
    driver.get(driver.execute_script('return window.location.href;'))
78
    execute_in_page(load_script('common/indexeddb.js'))
79
    mock_broadcast(execute_in_page)
80

    
81
    database_contents = get_db_contents(execute_in_page)
82

    
83
    assert set(execute_in_page('returnval(get_repos());')) == urls_v2
84

    
85
    # Verify that url does not get updated when there is no database update.
86
    execute_in_page('returnval(del_repo(arguments[0]));', v2_url)
87
    execute_in_page('returnval(set_repo(arguments[0]));', v1_url)
88

    
89
    driver.get(driver.execute_script('return window.location.href;'))
90
    execute_in_page(load_script('common/indexeddb.js'))
91
    mock_broadcast(execute_in_page)
92

    
93
    database_contents = get_db_contents(execute_in_page)
94

    
95
    assert set(execute_in_page('returnval(get_repos());')) == urls_v1
96

    
97
@pytest.mark.get_page('https://gotmyowndoma.in')
98
def test_haketilodb_item_modifications(driver, execute_in_page):
99
    """
100
    indexeddb.js facilitates operating on Haketilo's internal database.
101
    Verify database operations on mappings/resources work properly.
102
    """
103
    execute_in_page(load_script('common/indexeddb.js'))
104
    mock_broadcast(execute_in_page)
105

    
106
    # Start with no database.
107
    clear_indexeddb(execute_in_page)
108

    
109
    sample_item = make_sample_resource()
110
    sample_item['source_copyright'][0]['extra_prop'] = True
111

    
112
    execute_in_page(
113
        '''{
114
        const promise = start_items_transaction(["resource"], arguments[1])
115
            .then(ctx => save_item(arguments[0], ctx).then(() => ctx))
116
            .then(finalize_transaction);
117
        returnval(promise);
118
        }''',
119
        sample_item, {'sha256': sample_files_by_sha256})
120

    
121
    database_contents = get_db_contents(execute_in_page)
122

    
123
    assert len(database_contents['file']) == 4
124
    assert all([sample_files_by_sha256[file['sha256']] == file['contents']
125
                for file in database_contents['file']])
126
    assert all([len(file) == 2 for file in database_contents['file']])
127

    
128
    assert len(database_contents['file_uses']) == 4
129
    assert all([uses['uses'] == 1 for uses in database_contents['file_uses']])
130
    assert set([uses['sha256'] for uses in database_contents['file_uses']]) \
131
        == set([file['sha256'] for file in database_contents['file']])
132

    
133
    assert database_contents['mapping'] == []
134
    assert database_contents['resource'] == [sample_item]
135

    
136
    # See if trying to add an item without providing all its files ends in an
137
    # exception and aborts the transaction as it should.
138
    sample_item['scripts'].append(sample_file_ref('combined.js'))
139
    incomplete_files = {**sample_files_by_sha256}
140
    incomplete_files.pop(sample_files['combined.js']['sha256'])
141
    exception = execute_in_page(
142
        '''{
143
        const args = arguments;
144
        async function try_add_item()
145
        {
146
            const context =
147
                await start_items_transaction(["resource"], args[1]);
148
            try {
149
                await save_item(args[0], context);
150
                await finalize_transaction(context);
151
                return;
152
            } catch(e) {
153
                return e;
154
            }
155
        }
156
        returnval(try_add_item());
157
        }''',
158
        sample_item, {'sha256': incomplete_files})
159

    
160
    previous_database_contents = database_contents
161
    database_contents = get_db_contents(execute_in_page)
162

    
163
    assert 'file not present' in exception
164
    for key, val in database_contents.items():
165
        keyfun = lambda item: item.get('sha256') or item['identifier']
166
        assert sorted(previous_database_contents[key], key=keyfun) \
167
            == sorted(val,                             key=keyfun)
168

    
169
    # See if adding another item that partially uses first's files works OK.
170
    sample_item = make_sample_mapping()
171
    database_contents = execute_in_page(
172
        '''{
173
        const promise = start_items_transaction(["mapping"], arguments[1])
174
            .then(ctx => save_item(arguments[0], ctx).then(() => ctx))
175
            .then(finalize_transaction);
176
        returnval(promise);
177
        }''',
178
        sample_item, {'sha256': sample_files_by_sha256})
179

    
180
    database_contents = get_db_contents(execute_in_page)
181

    
182
    names = ['README.md', 'report.spdx', 'LICENSES/somelicense.txt', 'hello.js',
183
             'bye.js']
184
    sample_files_list = [sample_files[name] for name in names]
185
    uses_list = [1, 2, 1, 1, 1]
186

    
187
    uses = dict([(uses['sha256'], uses['uses'])
188
                 for uses in database_contents['file_uses']])
189
    assert uses  == dict([(file['sha256'], nr)
190
                          for file, nr in zip(sample_files_list, uses_list)])
191

    
192
    files = dict([(file['sha256'], file['contents'])
193
                  for file in database_contents['file']])
194
    assert files == dict([(file['sha256'], file['contents'])
195
                          for file in sample_files_list])
196

    
197
    del database_contents['resource'][0]['source_copyright'][0]['extra_prop']
198
    assert database_contents['resource'] == [make_sample_resource()]
199
    assert database_contents['mapping']  == [sample_item]
200

    
201
    # Try removing the items to get an empty database again.
202
    results = [None, None]
203
    for i, item_type in enumerate(['resource', 'mapping']):
204
         execute_in_page(
205
            f'''{{
206
            const remover = remove_{item_type};
207
            const promise =
208
                start_items_transaction(["{item_type}"], {{}})
209
                .then(ctx => remover('helloapple', ctx).then(() => ctx))
210
                .then(finalize_transaction);
211
            returnval(promise);
212
            }}''')
213

    
214
         results[i] = get_db_contents(execute_in_page)
215

    
216
    names = ['README.md', 'report.spdx']
217
    sample_files_list = [sample_files[name] for name in names]
218
    uses_list = [1, 1]
219

    
220
    uses = dict([(uses['sha256'], uses['uses'])
221
                 for uses in results[0]['file_uses']])
222
    assert uses  == dict([(file['sha256'], 1) for file in sample_files_list])
223

    
224
    files = dict([(file['sha256'], file['contents'])
225
                  for file in results[0]['file']])
226
    assert files == dict([(file['sha256'], file['contents'])
227
                          for file in sample_files_list])
228

    
229
    assert results[0]['resource'] == []
230
    assert results[0]['mapping'] == [sample_item]
231

    
232
    assert results[1] == dict([(key, []) for key in  results[0].keys()])
233

    
234
    # Try initializing an empty database with sample initial data object.
235
    sample_resource = make_sample_resource()
236
    sample_mapping = make_sample_mapping()
237
    initial_data = {
238
        'resource': {
239
            'helloapple': {
240
                '1.12':   sample_resource,
241
                '0.9':    'something_that_should_get_ignored',
242
                '1':      'something_that_should_get_ignored',
243
                '1.1':    'something_that_should_get_ignored',
244
                '1.11.1': 'something_that_should_get_ignored',
245
            }
246
        },
247
        'mapping': {
248
            'helloapple': {
249
                '0.1.1': sample_mapping
250
            }
251
        },
252
        'file': {
253
            'sha256': sample_files_by_sha256
254
        }
255
    }
256

    
257
    clear_indexeddb(execute_in_page)
258
    execute_in_page('initial_data = arguments[0];', initial_data)
259
    database_contents = get_db_contents(execute_in_page)
260

    
261
    assert database_contents['resource'] == [sample_resource]
262
    assert database_contents['mapping']  == [sample_mapping]
263

    
264
@pytest.mark.get_page('https://gotmyowndoma.in')
265
def test_haketilodb_settings(driver, execute_in_page):
266
    """
267
    indexeddb.js facilitates operating on Haketilo's internal database.
268
    Verify assigning/retrieving values of simple "setting" item works properly.
269
    """
270
    execute_in_page(load_script('common/indexeddb.js'))
271
    mock_broadcast(execute_in_page)
272

    
273
    # Start with no database.
274
    clear_indexeddb(execute_in_page)
275

    
276
    assert get_db_contents(execute_in_page)['setting'] == []
277

    
278
    assert execute_in_page('returnval(get_setting("option15"));') == None
279

    
280
    execute_in_page('returnval(set_setting("option15", "disable"));')
281
    assert execute_in_page('returnval(get_setting("option15"));') == 'disable'
282

    
283
    execute_in_page('returnval(set_setting("option15", "enable"));')
284
    assert execute_in_page('returnval(get_setting("option15"));') == 'enable'
285

    
286
@pytest.mark.get_page('https://gotmyowndoma.in')
287
def test_haketilodb_allowing(driver, execute_in_page):
288
    """
289
    indexeddb.js facilitates operating on Haketilo's internal database.
290
    Verify changing the "blocking" configuration for a URL works properly.
291
    """
292
    execute_in_page(load_script('common/indexeddb.js'))
293
    mock_broadcast(execute_in_page)
294

    
295
    # Start with no database.
296
    clear_indexeddb(execute_in_page)
297

    
298
    assert get_db_contents(execute_in_page)['blocking'] == []
299

    
300
    def run_with_sample_url(expr):
301
        return execute_in_page(f'returnval({expr});', 'https://example.com/**')
302

    
303
    assert None == run_with_sample_url('get_allowing(arguments[0])')
304

    
305
    run_with_sample_url('set_disallowed(arguments[0])')
306
    assert False == run_with_sample_url('get_allowing(arguments[0])')
307

    
308
    run_with_sample_url('set_allowed(arguments[0])')
309
    assert True == run_with_sample_url('get_allowing(arguments[0])')
310

    
311
    run_with_sample_url('set_default_allowing(arguments[0])')
312
    assert None == run_with_sample_url('get_allowing(arguments[0])')
313

    
314
@pytest.mark.get_page('https://gotmyowndoma.in')
315
def test_haketilodb_repos(driver, execute_in_page):
316
    """
317
    indexeddb.js facilitates operating on Haketilo's internal database.
318
    Verify operations on repositories list work properly.
319
    """
320
    execute_in_page(load_script('common/indexeddb.js'))
321
    mock_broadcast(execute_in_page)
322

    
323
    # Start with no database.
324
    clear_indexeddb(execute_in_page)
325

    
326
    assert get_db_contents(execute_in_page)['repo'] == []
327

    
328
    sample_urls = ['https://hdrlla.example.com/', 'https://hdrlla.example.org']
329

    
330
    assert [] == execute_in_page('returnval(get_repos());')
331

    
332
    execute_in_page('returnval(set_repo(arguments[0]));', sample_urls[0])
333
    assert [sample_urls[0]] == execute_in_page('returnval(get_repos());')
334

    
335
    execute_in_page('returnval(set_repo(arguments[0]));', sample_urls[1])
336
    assert set(sample_urls) == set(execute_in_page('returnval(get_repos());'))
337

    
338
    execute_in_page('returnval(del_repo(arguments[0]));', sample_urls[0])
339
    assert [sample_urls[1]] == execute_in_page('returnval(get_repos());')
340

    
341
test_page_html = '''
342
<!DOCTYPE html>
343
<script src="/testpage.js"></script>
344
<body>
345
</body>
346
'''
347

    
348
@pytest.mark.ext_data({
349
    'background_script': broker_js,
350
    'test_page':         test_page_html,
351
    'extra_files': {
352
        'testpage.js':   lambda: load_script('common/indexeddb.js')
353
    }
354
})
355
@pytest.mark.usefixtures('webextension')
356
def test_haketilodb_track(driver, execute_in_page, wait_elem_text):
357
    """
358
    Verify IndexedDB object change notifications are correctly broadcasted
359
    through extension's background script and allow for object store contents
360
    to be tracked in any execution context.
361
    """
362
    # Let's open the same extension's test page in a second window. Window 1
363
    # will be used to make changes to IndexedDB and window 0 to "track" those
364
    # changes.
365
    driver.execute_script('window.open(window.location.href, "_blank");')
366
    WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) == 2)
367
    windows = [*driver.window_handles]
368

    
369
    # Create elements that will have tracked data inserted under them.
370
    driver.switch_to.window(windows[0])
371
    execute_in_page(
372
        '''
373
        for (const store_name of trackable) {
374
            const h2 = document.createElement("h2");
375
            h2.innerText = store_name;
376
            document.body.append(h2);
377

    
378
            const ul = document.createElement("ul");
379
            ul.id = store_name;
380
            document.body.append(ul);
381
        }
382
        ''')
383

    
384
    # Mock initial_data.
385
    sample_resource = make_sample_resource()
386
    sample_mapping = make_sample_mapping()
387
    initial_data = {
388
        'resource': {
389
            'helloapple': {
390
                '1.0': sample_resource
391
            }
392
        },
393
        'mapping': {
394
            'helloapple': {
395
                '0.1.1': sample_mapping
396
            }
397
        },
398
        'file': {
399
            'sha256': sample_files_by_sha256
400
        }
401
    }
402
    driver.switch_to.window(windows[1])
403
    execute_in_page('initial_data = arguments[0];', initial_data)
404
    execute_in_page('returnval(set_setting("option15", "123"));')
405
    execute_in_page('returnval(set_repo("https://hydril.la"));')
406
    execute_in_page('returnval(set_disallowed("file:///*"));')
407

    
408
    # See if track.*() functions properly return the already-existing items.
409
    driver.switch_to.window(windows[0])
410
    execute_in_page(
411
        '''
412
        function update_item(store_name, change)
413
        {
414
            const elem_id = `${store_name}_${change.key}`;
415
            let elem = document.getElementById(elem_id);
416
            elem = elem || document.createElement("li");
417
            elem.id = elem_id;
418
            elem.innerText = JSON.stringify(change.new_val);
419
            document.getElementById(store_name).append(elem);
420
            if (change.new_val === undefined)
421
                elem.remove();
422
        }
423

    
424
        let resource_tracking, resource_items, mapping_tracking, mapping_items;
425

    
426
        async function start_reporting()
427
        {
428
            const props = new Map(stores.map(([sn, opt]) => [sn, opt.keyPath]));
429
            for (const store_name of trackable) {
430
                [tracking, items] =
431
                    await track[store_name](ch => update_item(store_name, ch));
432
                const prop = props.get(store_name);
433
                for (const item of items)
434
                    update_item(store_name, {key: item[prop], new_val: item});
435
            }
436
        }
437

    
438
        returnval(start_reporting());
439
        ''')
440

    
441
    item_counts = execute_in_page(
442
        '''{
443
        const childcount = id => document.getElementById(id).childElementCount;
444
        returnval(trackable.map(childcount));
445
        }''')
446
    assert item_counts == [1 for _ in item_counts]
447
    for elem_id, json_value in [
448
            ('resource_helloapple', sample_resource),
449
            ('mapping_helloapple', sample_mapping),
450
            ('setting_option15', {'name': 'option15', 'value': '123'}),
451
            ('repo_https://hydril.la', {'url': 'https://hydril.la'}),
452
            ('blocking_file:///*', {'pattern': 'file:///*', 'allow': False})
453
    ]:
454
        assert json.loads(driver.find_element_by_id(elem_id).text) == json_value
455

    
456
    # See if item additions get tracked properly.
457
    driver.switch_to.window(windows[1])
458
    sample_resource2 = make_sample_resource()
459
    sample_resource2['identifier'] = 'helloapple-copy'
460
    sample_mapping2 = make_sample_mapping()
461
    sample_mapping2['identifier'] = 'helloapple-copy'
462
    sample_data = {
463
        'resource': {
464
            'helloapple-copy': {
465
                '1.0': sample_resource2
466
            }
467
        },
468
        'mapping': {
469
            'helloapple-copy': {
470
                '0.1.1': sample_mapping2
471
            }
472
        },
473
        'file': {
474
            'sha256': sample_files_by_sha256
475
        },
476
        'repo': [
477
            'https://hydril2.la/'
478
        ]
479
    }
480
    execute_in_page('returnval(save_items(arguments[0]));', sample_data)
481
    execute_in_page('returnval(set_setting("option22", "abc"));')
482
    execute_in_page('returnval(set_repo("https://hydril3.la/"));')
483
    execute_in_page('returnval(set_allowed("ftp://a.bc/"));')
484

    
485
    driver.switch_to.window(windows[0])
486
    driver.implicitly_wait(10)
487
    for elem_id, json_value in [
488
            ('resource_helloapple-copy', sample_resource2),
489
            ('mapping_helloapple-copy', sample_mapping2),
490
            ('setting_option22', {'name': 'option22', 'value': 'abc'}),
491
            ('repo_https://hydril2.la/', {'url': 'https://hydril2.la/'}),
492
            ('repo_https://hydril3.la/', {'url': 'https://hydril3.la/'}),
493
            ('blocking_ftp://a.bc/', {'pattern': 'ftp://a.bc/', 'allow': True})
494
    ]:
495
        assert json.loads(driver.find_element_by_id(elem_id).text) == json_value
496
    driver.implicitly_wait(0)
497

    
498
    # See if item deletions/modifications get tracked properly.
499
    driver.switch_to.window(windows[1])
500
    execute_in_page(
501
        '''{
502
        async function change_remove_items()
503
        {
504
            const store_names = ["resource", "mapping"];
505
            const ctx = await start_items_transaction(store_names, {});
506
            await remove_resource("helloapple", ctx);
507
            await remove_mapping("helloapple-copy", ctx);
508
            await finalize_transaction(ctx);
509
            await set_setting("option22", null);
510
            await del_repo("https://hydril.la");
511
            await set_default_allowing("file:///*");
512
            await set_disallowed("ftp://a.bc/");
513
        }
514
        returnval(change_remove_items());
515
        }''')
516

    
517
    removed_ids = ['mapping_helloapple-copy', 'resource_helloapple',
518
                   'repo_https://hydril.la', 'blocking_file:///*']
519
    def condition_items_absent_and_changed(driver):
520
        for id in removed_ids:
521
            try:
522
                driver.find_element_by_id(id)
523
                return False
524
            except WebDriverException:
525
                pass
526

    
527
        option_text = driver.find_element_by_id('setting_option22').text
528
        blocking_text = driver.find_element_by_id('blocking_ftp://a.bc/').text
529
        return (json.loads(option_text)['value'] == None and
530
                json.loads(blocking_text)['allow'] == False)
531

    
532
    driver.switch_to.window(windows[0])
533
    WebDriverWait(driver, 10).until(condition_items_absent_and_changed)
(9-9/26)