Project

General

Profile

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

haketilo / test / unit / test_indexeddb.py @ 5c58b3d6

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, sample_files_by_hash)
77

    
78
    database_contents = get_db_contents(execute_in_page)
79

    
80
    assert len(database_contents['files']) == 4
81
    assert all([sample_files_by_hash[file['hash_key']] == file['contents']
82
                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
    assert set([uses['hash_key'] for uses in database_contents['file_uses']]) \
88
        == set([file['hash_key'] for file in database_contents['files']])
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_hash}
97
    incomplete_files.pop(sample_files['combined.js']['hash_key'])
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, 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('hash_key') 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, sample_files_by_hash)
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['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
    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['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
    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
        '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

    
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
    assert database_contents['resource'] == [sample_resource]
217
    assert database_contents['mapping']  == [sample_mapping]
218

    
219
@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
    Verify assigning/retrieving values of simple "settings" item works properly.
224
    """
225
    execute_in_page(load_script('common/indexeddb.js'))
226
    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
@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
    execute_in_page(load_script('common/indexeddb.js'))
248
    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
    execute_in_page(load_script('common/indexeddb.js'))
276
    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
test_page_html = '''
297
<!DOCTYPE html>
298
<script src="/testpage.js"></script>
299
<body>
300
</body>
301
'''
302

    
303
@pytest.mark.ext_data({
304
    'background_script': broker_js,
305
    'test_page':         test_page_html,
306
    'extra_files': {
307
        'testpage.js':   lambda: load_script('common/indexeddb.js')
308
    }
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
    # 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
    # changes.
320
    driver.execute_script('window.open(window.location.href, "_blank");')
321
    windows = [*driver.window_handles]
322
    assert len(windows) == 2
323

    
324
    # 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
    # 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
    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

    
360
    # See if track.*() functions properly return the already-existing items.
361
    driver.switch_to.window(windows[0])
362
    execute_in_page(
363
        '''
364
        function update_item(store_name, change)
365
        {
366
            const elem_id = `${store_name}_${change.key}`;
367
            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
        async function start_reporting()
379
        {
380
            const props = new Map(stores.map(([sn, opt]) => [sn, opt.keyPath]));
381
            for (const store_name of trackable) {
382
                [tracking, items] =
383
                    await track[store_name](ch => update_item(store_name, ch));
384
                const prop = props.get(store_name);
385
                for (const item of items)
386
                    update_item(store_name, {key: item[prop], new_val: item});
387
            }
388
        }
389

    
390
        returnval(start_reporting());
391
        ''')
392

    
393
    item_counts = execute_in_page(
394
        '''{
395
        const childcount = id => document.getElementById(id).childElementCount;
396
        returnval(trackable.map(childcount));
397
        }''')
398
    assert item_counts == [1 for _ in item_counts]
399
    for elem_id, json_value in [
400
            ('resource_helloapple', sample_resource),
401
            ('mapping_helloapple', sample_mapping),
402
            ('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

    
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
    execute_in_page('returnval(save_items(arguments[0]));', sample_data)
428
    execute_in_page('returnval(set_setting("option22", "abc"));')
429
    execute_in_page('returnval(set_repo("https://hydril2.la"));')
430
    execute_in_page('returnval(set_allowed("ftp://a.bc/"));')
431

    
432
    driver.switch_to.window(windows[0])
433
    driver.implicitly_wait(10)
434
    for elem_id, json_value in [
435
            ('resource_helloapple-copy', sample_resource2),
436
            ('mapping_helloapple-copy', sample_mapping2),
437
            ('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
    driver.implicitly_wait(0)
443

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

    
463
    removed_ids = ['mapping_helloapple-copy', 'resource_helloapple',
464
                   'repos_https://hydril.la', 'blocking_file:///*']
465
    def condition_items_absent_and_changed(driver):
466
        for id in removed_ids:
467
            try:
468
                driver.find_element_by_id(id)
469
                return False
470
            except WebDriverException:
471
                pass
472

    
473
        option_text = driver.find_element_by_id('settings_option22').text
474
        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

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