Project

General

Profile

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

haketilo / test / unit / test_indexeddb.py @ ad69f9c8

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
    WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) == 2)
322
    windows = [*driver.window_handles]
323

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

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

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

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

    
377
        let resource_tracking, resource_items, mapping_tracking, mapping_items;
378

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

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

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

    
409
    # See if item additions get tracked properly.
410
    driver.switch_to.window(windows[1])
411
    sample_resource2 = make_sample_resource()
412
    sample_resource2['identifier'] = 'helloapple-copy'
413
    sample_mapping2 = make_sample_mapping()
414
    sample_mapping2['identifier'] = 'helloapple-copy'
415
    sample_data = {
416
        'resources': {
417
            'helloapple-copy': {
418
                '1.0': sample_resource2
419
            }
420
        },
421
        'mappings': {
422
            'helloapple-copy': {
423
                '0.1.1': sample_mapping2
424
            }
425
        },
426
        'files': sample_files_by_hash
427
    }
428
    execute_in_page('returnval(save_items(arguments[0]));', sample_data)
429
    execute_in_page('returnval(set_setting("option22", "abc"));')
430
    execute_in_page('returnval(set_repo("https://hydril2.la"));')
431
    execute_in_page('returnval(set_allowed("ftp://a.bc/"));')
432

    
433
    driver.switch_to.window(windows[0])
434
    driver.implicitly_wait(10)
435
    for elem_id, json_value in [
436
            ('resource_helloapple-copy', sample_resource2),
437
            ('mapping_helloapple-copy', sample_mapping2),
438
            ('settings_option22', {'name': 'option22', 'value': 'abc'}),
439
            ('repos_https://hydril2.la', {'url': 'https://hydril2.la'}),
440
            ('blocking_ftp://a.bc/', {'pattern': 'ftp://a.bc/', 'allow': True})
441
    ]:
442
        assert json.loads(driver.find_element_by_id(elem_id).text) == json_value
443
    driver.implicitly_wait(0)
444

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

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

    
474
        option_text = driver.find_element_by_id('settings_option22').text
475
        blocking_text = driver.find_element_by_id('blocking_ftp://a.bc/').text
476
        return (json.loads(option_text)['value'] == None and
477
                json.loads(blocking_text)['allow'] == False)
478

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