Project

General

Profile

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

haketilo / test / unit / test_indexeddb.py @ 92fc67cf

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.webdriver.support import expected_conditions as EC
25
from selenium.common.exceptions import WebDriverException
26

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

    
30
# 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
            sample_file_ref('report.spdx'),
37
            sample_file_ref('LICENSES/somelicense.txt')
38
        ],
39
        'type': 'resource',
40
        'identifier': 'helloapple',
41
        'scripts': [sample_file_ref('hello.js'), sample_file_ref('bye.js')]
42
    }
43

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

    
54
@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
    execute_in_page(load_script('common/indexeddb.js'))
61
    mock_broadcast(execute_in_page)
62

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

    
66
    sample_item = make_sample_resource()
67
    sample_item['source_copyright'][0]['extra_prop'] = True
68

    
69
    execute_in_page(
70
        '''{
71
        const promise = start_items_transaction(["resource"], arguments[1])
72
            .then(ctx => save_item(arguments[0], ctx).then(() => ctx))
73
            .then(finalize_transaction);
74
        returnval(promise);
75
        }''',
76
        sample_item, {'sha256': sample_files_by_sha256})
77

    
78
    database_contents = get_db_contents(execute_in_page)
79

    
80
    assert len(database_contents['file']) == 4
81
    assert all([sample_files_by_sha256[file['sha256']] == file['contents']
82
                for file in database_contents['file']])
83
    assert all([len(file) == 2 for file in database_contents['file']])
84

    
85
    assert len(database_contents['file_uses']) == 4
86
    assert all([uses['uses'] == 1 for uses in database_contents['file_uses']])
87
    assert set([uses['sha256'] for uses in database_contents['file_uses']]) \
88
        == set([file['sha256'] for file in database_contents['file']])
89

    
90
    assert database_contents['mapping'] == []
91
    assert database_contents['resource'] == [sample_item]
92

    
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
    sample_item['scripts'].append(sample_file_ref('combined.js'))
96
    incomplete_files = {**sample_files_by_sha256}
97
    incomplete_files.pop(sample_files['combined.js']['sha256'])
98
    exception = execute_in_page(
99
        '''{
100
        const args = arguments;
101
        async function try_add_item()
102
        {
103
            const context =
104
                await start_items_transaction(["resource"], args[1]);
105
            try {
106
                await save_item(args[0], context);
107
                await finalize_transaction(context);
108
                return;
109
            } catch(e) {
110
                return e;
111
            }
112
        }
113
        returnval(try_add_item());
114
        }''',
115
        sample_item, {'sha256': incomplete_files})
116

    
117
    previous_database_contents = database_contents
118
    database_contents = get_db_contents(execute_in_page)
119

    
120
    assert 'file not present' in exception
121
    for key, val in database_contents.items():
122
        keyfun = lambda item: item.get('sha256') or item['identifier']
123
        assert sorted(previous_database_contents[key], key=keyfun) \
124
            == sorted(val,                             key=keyfun)
125

    
126
    # See if adding another item that partially uses first's files works OK.
127
    sample_item = make_sample_mapping()
128
    database_contents = execute_in_page(
129
        '''{
130
        const promise = start_items_transaction(["mapping"], arguments[1])
131
            .then(ctx => save_item(arguments[0], ctx).then(() => ctx))
132
            .then(finalize_transaction);
133
        returnval(promise);
134
        }''',
135
        sample_item, {'sha256': sample_files_by_sha256})
136

    
137
    database_contents = get_db_contents(execute_in_page)
138

    
139
    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['sha256'], uses['uses'])
145
                 for uses in database_contents['file_uses']])
146
    assert uses  == dict([(file['sha256'], nr)
147
                          for file, nr in zip(sample_files_list, uses_list)])
148

    
149
    files = dict([(file['sha256'], file['contents'])
150
                  for file in database_contents['file']])
151
    assert files == dict([(file['sha256'], file['contents'])
152
                          for file in sample_files_list])
153

    
154
    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

    
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
         execute_in_page(
162
            f'''{{
163
            const remover = remove_{item_type};
164
            const promise =
165
                start_items_transaction(["{item_type}"], {{}})
166
                .then(ctx => remover('helloapple', ctx).then(() => ctx))
167
                .then(finalize_transaction);
168
            returnval(promise);
169
            }}''')
170

    
171
         results[i] = get_db_contents(execute_in_page)
172

    
173
    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['sha256'], uses['uses'])
178
                 for uses in results[0]['file_uses']])
179
    assert uses  == dict([(file['sha256'], 1) for file in sample_files_list])
180

    
181
    files = dict([(file['sha256'], file['contents'])
182
                  for file in results[0]['file']])
183
    assert files == dict([(file['sha256'], file['contents'])
184
                          for file in sample_files_list])
185

    
186
    assert results[0]['resource'] == []
187
    assert results[0]['mapping'] == [sample_item]
188

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

    
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
        'resource': {
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
        'mapping': {
205
            'helloapple': {
206
                '0.1.1': sample_mapping
207
            }
208
        },
209
        'file': {
210
            'sha256': sample_files_by_sha256
211
        }
212
    }
213

    
214
    clear_indexeddb(execute_in_page)
215
    execute_in_page('initial_data = arguments[0];', initial_data)
216
    database_contents = get_db_contents(execute_in_page)
217

    
218
    assert database_contents['resource'] == [sample_resource]
219
    assert database_contents['mapping']  == [sample_mapping]
220

    
221
@pytest.mark.get_page('https://gotmyowndoma.in')
222
def test_haketilodb_settings(driver, execute_in_page):
223
    """
224
    indexeddb.js facilitates operating on Haketilo's internal database.
225
    Verify assigning/retrieving values of simple "setting" item works properly.
226
    """
227
    execute_in_page(load_script('common/indexeddb.js'))
228
    mock_broadcast(execute_in_page)
229

    
230
    # Start with no database.
231
    clear_indexeddb(execute_in_page)
232

    
233
    assert get_db_contents(execute_in_page)['setting'] == []
234

    
235
    assert execute_in_page('returnval(get_setting("option15"));') == None
236

    
237
    execute_in_page('returnval(set_setting("option15", "disable"));')
238
    assert execute_in_page('returnval(get_setting("option15"));') == 'disable'
239

    
240
    execute_in_page('returnval(set_setting("option15", "enable"));')
241
    assert execute_in_page('returnval(get_setting("option15"));') == 'enable'
242

    
243
@pytest.mark.get_page('https://gotmyowndoma.in')
244
def test_haketilodb_allowing(driver, execute_in_page):
245
    """
246
    indexeddb.js facilitates operating on Haketilo's internal database.
247
    Verify changing the "blocking" configuration for a URL works properly.
248
    """
249
    execute_in_page(load_script('common/indexeddb.js'))
250
    mock_broadcast(execute_in_page)
251

    
252
    # Start with no database.
253
    clear_indexeddb(execute_in_page)
254

    
255
    assert get_db_contents(execute_in_page)['blocking'] == []
256

    
257
    def run_with_sample_url(expr):
258
        return execute_in_page(f'returnval({expr});', 'https://example.com/**')
259

    
260
    assert None == run_with_sample_url('get_allowing(arguments[0])')
261

    
262
    run_with_sample_url('set_disallowed(arguments[0])')
263
    assert False == run_with_sample_url('get_allowing(arguments[0])')
264

    
265
    run_with_sample_url('set_allowed(arguments[0])')
266
    assert True == run_with_sample_url('get_allowing(arguments[0])')
267

    
268
    run_with_sample_url('set_default_allowing(arguments[0])')
269
    assert None == run_with_sample_url('get_allowing(arguments[0])')
270

    
271
@pytest.mark.get_page('https://gotmyowndoma.in')
272
def test_haketilodb_repos(driver, execute_in_page):
273
    """
274
    indexeddb.js facilitates operating on Haketilo's internal database.
275
    Verify operations on repositories list work properly.
276
    """
277
    execute_in_page(load_script('common/indexeddb.js'))
278
    mock_broadcast(execute_in_page)
279

    
280
    # Start with no database.
281
    clear_indexeddb(execute_in_page)
282

    
283
    assert get_db_contents(execute_in_page)['repo'] == []
284

    
285
    sample_urls = ['https://hdrlla.example.com/', 'https://hdrlla.example.org']
286

    
287
    assert [] == execute_in_page('returnval(get_repos());')
288

    
289
    execute_in_page('returnval(set_repo(arguments[0]));', sample_urls[0])
290
    assert [sample_urls[0]] == execute_in_page('returnval(get_repos());')
291

    
292
    execute_in_page('returnval(set_repo(arguments[0]));', sample_urls[1])
293
    assert set(sample_urls) == set(execute_in_page('returnval(get_repos());'))
294

    
295
    execute_in_page('returnval(del_repo(arguments[0]));', sample_urls[0])
296
    assert [sample_urls[1]] == execute_in_page('returnval(get_repos());')
297

    
298
test_page_html = '''
299
<!DOCTYPE html>
300
<script src="/testpage.js"></script>
301
<body>
302
</body>
303
'''
304

    
305
@pytest.mark.ext_data({
306
    'background_script': broker_js,
307
    'test_page':         test_page_html,
308
    'extra_files': {
309
        'testpage.js':   lambda: load_script('common/indexeddb.js')
310
    }
311
})
312
@pytest.mark.usefixtures('webextension')
313
def test_haketilodb_track(driver, execute_in_page, wait_elem_text):
314
    """
315
    Verify IndexedDB object change notifications are correctly broadcasted
316
    through extension's background script and allow for object store contents
317
    to be tracked in any execution context.
318
    """
319
    # Let's open the same extension's test page in a second window. Window 1
320
    # will be used to make changes to IndexedDB and window 0 to "track" those
321
    # changes.
322
    driver.execute_script('window.open(window.location.href, "_blank");')
323
    WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) == 2)
324
    windows = [*driver.window_handles]
325

    
326
    # Create elements that will have tracked data inserted under them.
327
    driver.switch_to.window(windows[0])
328
    execute_in_page(
329
        '''
330
        for (const store_name of trackable) {
331
            const h2 = document.createElement("h2");
332
            h2.innerText = store_name;
333
            document.body.append(h2);
334

    
335
            const ul = document.createElement("ul");
336
            ul.id = store_name;
337
            document.body.append(ul);
338
        }
339
        ''')
340

    
341
    # Mock initial_data.
342
    sample_resource = make_sample_resource()
343
    sample_mapping = make_sample_mapping()
344
    initial_data = {
345
        'resource': {
346
            'helloapple': {
347
                '1.0': sample_resource
348
            }
349
        },
350
        'mapping': {
351
            'helloapple': {
352
                '0.1.1': sample_mapping
353
            }
354
        },
355
        'file': {
356
            'sha256': sample_files_by_sha256
357
        }
358
    }
359
    driver.switch_to.window(windows[1])
360
    execute_in_page('initial_data = arguments[0];', initial_data)
361
    execute_in_page('returnval(set_setting("option15", "123"));')
362
    execute_in_page('returnval(set_repo("https://hydril.la"));')
363
    execute_in_page('returnval(set_disallowed("file:///*"));')
364

    
365
    # See if track.*() functions properly return the already-existing items.
366
    driver.switch_to.window(windows[0])
367
    execute_in_page(
368
        '''
369
        function update_item(store_name, change)
370
        {
371
            const elem_id = `${store_name}_${change.key}`;
372
            let elem = document.getElementById(elem_id);
373
            elem = elem || document.createElement("li");
374
            elem.id = elem_id;
375
            elem.innerText = JSON.stringify(change.new_val);
376
            document.getElementById(store_name).append(elem);
377
            if (change.new_val === undefined)
378
                elem.remove();
379
        }
380

    
381
        let resource_tracking, resource_items, mapping_tracking, mapping_items;
382

    
383
        async function start_reporting()
384
        {
385
            const props = new Map(stores.map(([sn, opt]) => [sn, opt.keyPath]));
386
            for (const store_name of trackable) {
387
                [tracking, items] =
388
                    await track[store_name](ch => update_item(store_name, ch));
389
                const prop = props.get(store_name);
390
                for (const item of items)
391
                    update_item(store_name, {key: item[prop], new_val: item});
392
            }
393
        }
394

    
395
        returnval(start_reporting());
396
        ''')
397

    
398
    item_counts = execute_in_page(
399
        '''{
400
        const childcount = id => document.getElementById(id).childElementCount;
401
        returnval(trackable.map(childcount));
402
        }''')
403
    assert item_counts == [1 for _ in item_counts]
404
    for elem_id, json_value in [
405
            ('resource_helloapple', sample_resource),
406
            ('mapping_helloapple', sample_mapping),
407
            ('setting_option15', {'name': 'option15', 'value': '123'}),
408
            ('repo_https://hydril.la', {'url': 'https://hydril.la'}),
409
            ('blocking_file:///*', {'pattern': 'file:///*', 'allow': False})
410
    ]:
411
        assert json.loads(driver.find_element_by_id(elem_id).text) == json_value
412

    
413
    # See if item additions get tracked properly.
414
    driver.switch_to.window(windows[1])
415
    sample_resource2 = make_sample_resource()
416
    sample_resource2['identifier'] = 'helloapple-copy'
417
    sample_mapping2 = make_sample_mapping()
418
    sample_mapping2['identifier'] = 'helloapple-copy'
419
    sample_data = {
420
        'resource': {
421
            'helloapple-copy': {
422
                '1.0': sample_resource2
423
            }
424
        },
425
        'mapping': {
426
            'helloapple-copy': {
427
                '0.1.1': sample_mapping2
428
            }
429
        },
430
        'file': {
431
            'sha256': sample_files_by_sha256
432
        }
433
    }
434
    execute_in_page('returnval(save_items(arguments[0]));', sample_data)
435
    execute_in_page('returnval(set_setting("option22", "abc"));')
436
    execute_in_page('returnval(set_repo("https://hydril2.la"));')
437
    execute_in_page('returnval(set_allowed("ftp://a.bc/"));')
438

    
439
    driver.switch_to.window(windows[0])
440
    driver.implicitly_wait(10)
441
    for elem_id, json_value in [
442
            ('resource_helloapple-copy', sample_resource2),
443
            ('mapping_helloapple-copy', sample_mapping2),
444
            ('setting_option22', {'name': 'option22', 'value': 'abc'}),
445
            ('repo_https://hydril2.la', {'url': 'https://hydril2.la'}),
446
            ('blocking_ftp://a.bc/', {'pattern': 'ftp://a.bc/', 'allow': True})
447
    ]:
448
        assert json.loads(driver.find_element_by_id(elem_id).text) == json_value
449
    driver.implicitly_wait(0)
450

    
451
    # See if item deletions/modifications get tracked properly.
452
    driver.switch_to.window(windows[1])
453
    execute_in_page(
454
        '''{
455
        async function change_remove_items()
456
        {
457
            const store_names = ["resource", "mapping"];
458
            const ctx = await start_items_transaction(store_names, {});
459
            await remove_resource("helloapple", ctx);
460
            await remove_mapping("helloapple-copy", ctx);
461
            await finalize_transaction(ctx);
462
            await set_setting("option22", null);
463
            await del_repo("https://hydril.la");
464
            await set_default_allowing("file:///*");
465
            await set_disallowed("ftp://a.bc/");
466
        }
467
        returnval(change_remove_items());
468
        }''')
469

    
470
    removed_ids = ['mapping_helloapple-copy', 'resource_helloapple',
471
                   'repo_https://hydril.la', 'blocking_file:///*']
472
    def condition_items_absent_and_changed(driver):
473
        for id in removed_ids:
474
            try:
475
                driver.find_element_by_id(id)
476
                return False
477
            except WebDriverException:
478
                pass
479

    
480
        option_text = driver.find_element_by_id('setting_option22').text
481
        blocking_text = driver.find_element_by_id('blocking_ftp://a.bc/').text
482
        return (json.loads(option_text)['value'] == None and
483
                json.loads(blocking_text)['allow'] == False)
484

    
485
    driver.switch_to.window(windows[0])
486
    WebDriverWait(driver, 10).until(condition_items_absent_and_changed)
(8-8/25)