Project

General

Profile

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

haketilo / test / unit / test_indexeddb.py @ 7218849a

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
def mock_broadcast(execute_in_page):
55
    execute_in_page(
56
        '''{
57
        const broadcast_mock = {};
58
        const nop = () => {};
59
        for (const key in broadcast)
60
            broadcast_mock[key] = nop;
61
        broadcast = broadcast_mock;
62
        }''')
63

    
64
@pytest.mark.get_page('https://gotmyowndoma.in')
65
def test_haketilodb_item_modifications(driver, execute_in_page):
66
    """
67
    indexeddb.js facilitates operating on Haketilo's internal database.
68
    Verify database operations on mappings/resources work properly.
69
    """
70
    execute_in_page(load_script('common/indexeddb.js'))
71
    mock_broadcast(execute_in_page)
72

    
73
    # Start with no database.
74
    clear_indexeddb(execute_in_page)
75

    
76
    sample_item = make_sample_resource()
77
    sample_item['source_copyright'][0]['extra_prop'] = True
78

    
79
    execute_in_page(
80
        '''{
81
        const promise = start_items_transaction(["resource"], arguments[1])
82
            .then(ctx => save_item(arguments[0], ctx).then(() => ctx))
83
            .then(finalize_transaction);
84
        returnval(promise);
85
        }''',
86
        sample_item, sample_files_by_hash)
87

    
88
    database_contents = get_db_contents(execute_in_page)
89

    
90
    assert len(database_contents['files']) == 4
91
    assert all([sample_files_by_hash[file['hash_key']] == file['contents']
92
                for file in database_contents['files']])
93
    assert all([len(file) == 2 for file in database_contents['files']])
94

    
95
    assert len(database_contents['file_uses']) == 4
96
    assert all([uses['uses'] == 1 for uses in database_contents['file_uses']])
97
    assert set([uses['hash_key'] for uses in database_contents['file_uses']]) \
98
        == set([file['hash_key'] for file in database_contents['files']])
99

    
100
    assert database_contents['mapping'] == []
101
    assert database_contents['resource'] == [sample_item]
102

    
103
    # See if trying to add an item without providing all its files ends in an
104
    # exception and aborts the transaction as it should.
105
    sample_item['scripts'].append(sample_file_ref('combined.js'))
106
    incomplete_files = {**sample_files_by_hash}
107
    incomplete_files.pop(sample_files['combined.js']['hash_key'])
108
    exception = execute_in_page(
109
        '''{
110
        const args = arguments;
111
        async function try_add_item()
112
        {
113
            const context =
114
                await start_items_transaction(["resource"], args[1]);
115
            try {
116
                await save_item(args[0], context);
117
                await finalize_transaction(context);
118
                return;
119
            } catch(e) {
120
                return e;
121
            }
122
        }
123
        returnval(try_add_item());
124
        }''',
125
        sample_item, incomplete_files)
126

    
127
    previous_database_contents = database_contents
128
    database_contents = get_db_contents(execute_in_page)
129

    
130
    assert 'file not present' in exception
131
    for key, val in database_contents.items():
132
        keyfun = lambda item: item.get('hash_key') or item['identifier']
133
        assert sorted(previous_database_contents[key], key=keyfun) \
134
            == sorted(val,                             key=keyfun)
135

    
136
    # See if adding another item that partially uses first's files works OK.
137
    sample_item = make_sample_mapping()
138
    database_contents = execute_in_page(
139
        '''{
140
        const promise = start_items_transaction(["mapping"], arguments[1])
141
            .then(ctx => save_item(arguments[0], ctx).then(() => ctx))
142
            .then(finalize_transaction);
143
        returnval(promise);
144
        }''',
145
        sample_item, sample_files_by_hash)
146

    
147
    database_contents = get_db_contents(execute_in_page)
148

    
149
    names = ['README.md', 'report.spdx', 'LICENSES/somelicense.txt', 'hello.js',
150
             'bye.js']
151
    sample_files_list = [sample_files[name] for name in names]
152
    uses_list = [1, 2, 1, 1, 1]
153

    
154
    uses = dict([(uses['hash_key'], uses['uses'])
155
                 for uses in database_contents['file_uses']])
156
    assert uses  == dict([(file['hash_key'], nr)
157
                          for file, nr in zip(sample_files_list, uses_list)])
158

    
159
    files = dict([(file['hash_key'], file['contents'])
160
                  for file in database_contents['files']])
161
    assert files == dict([(file['hash_key'], file['contents'])
162
                          for file in sample_files_list])
163

    
164
    del database_contents['resource'][0]['source_copyright'][0]['extra_prop']
165
    assert database_contents['resource'] == [make_sample_resource()]
166
    assert database_contents['mapping']  == [sample_item]
167

    
168
    # Try removing the items to get an empty database again.
169
    results = [None, None]
170
    for i, item_type in enumerate(['resource', 'mapping']):
171
         execute_in_page(
172
            f'''{{
173
            const remover = remove_{item_type};
174
            const promise =
175
                start_items_transaction(["{item_type}"], {{}})
176
                .then(ctx => remover('helloapple', ctx).then(() => ctx))
177
                .then(finalize_transaction);
178
            returnval(promise);
179
            }}''')
180

    
181
         results[i] = get_db_contents(execute_in_page)
182

    
183
    names = ['README.md', 'report.spdx']
184
    sample_files_list = [sample_files[name] for name in names]
185
    uses_list = [1, 1]
186

    
187
    uses = dict([(uses['hash_key'], uses['uses'])
188
                 for uses in results[0]['file_uses']])
189
    assert uses  == dict([(file['hash_key'], 1) for file in sample_files_list])
190

    
191
    files = dict([(file['hash_key'], file['contents'])
192
                  for file in results[0]['files']])
193
    assert files == dict([(file['hash_key'], file['contents'])
194
                          for file in sample_files_list])
195

    
196
    assert results[0]['resource'] == []
197
    assert results[0]['mapping'] == [sample_item]
198

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

    
201
    # Try initializing an empty database with sample initial data object.
202
    sample_resource = make_sample_resource()
203
    sample_mapping = make_sample_mapping()
204
    initial_data = {
205
        'resources': {
206
            'helloapple': {
207
                '1.12':   sample_resource,
208
                '0.9':    'something_that_should_get_ignored',
209
                '1':      'something_that_should_get_ignored',
210
                '1.1':    'something_that_should_get_ignored',
211
                '1.11.1': 'something_that_should_get_ignored',
212
            }
213
        },
214
        'mappings': {
215
            'helloapple': {
216
                '0.1.1': sample_mapping
217
            }
218
        },
219
        'files': sample_files_by_hash
220
    }
221

    
222
    clear_indexeddb(execute_in_page)
223
    execute_in_page('initial_data = arguments[0];', initial_data)
224
    database_contents = get_db_contents(execute_in_page)
225

    
226
    assert database_contents['resource'] == [sample_resource]
227
    assert database_contents['mapping']  == [sample_mapping]
228

    
229
@pytest.mark.get_page('https://gotmyowndoma.in')
230
def test_haketilodb_settings(driver, execute_in_page):
231
    """
232
    indexeddb.js facilitates operating on Haketilo's internal database.
233
    Verify assigning/retrieving values of simple "settings" item works properly.
234
    """
235
    execute_in_page(load_script('common/indexeddb.js'))
236
    mock_broadcast(execute_in_page)
237

    
238
    # Start with no database.
239
    clear_indexeddb(execute_in_page)
240

    
241
    assert get_db_contents(execute_in_page)['settings'] == []
242

    
243
    assert execute_in_page('returnval(get_setting("option15"));') == None
244

    
245
    execute_in_page('returnval(set_setting("option15", "disable"));')
246
    assert execute_in_page('returnval(get_setting("option15"));') == 'disable'
247

    
248
    execute_in_page('returnval(set_setting("option15", "enable"));')
249
    assert execute_in_page('returnval(get_setting("option15"));') == 'enable'
250

    
251
@pytest.mark.get_page('https://gotmyowndoma.in')
252
def test_haketilodb_allowing(driver, execute_in_page):
253
    """
254
    indexeddb.js facilitates operating on Haketilo's internal database.
255
    Verify changing the "blocking" configuration for a URL works properly.
256
    """
257
    execute_in_page(load_script('common/indexeddb.js'))
258
    mock_broadcast(execute_in_page)
259

    
260
    # Start with no database.
261
    clear_indexeddb(execute_in_page)
262

    
263
    assert get_db_contents(execute_in_page)['blocking'] == []
264

    
265
    def run_with_sample_url(expr):
266
        return execute_in_page(f'returnval({expr});', 'https://example.com/**')
267

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

    
270
    run_with_sample_url('set_disallowed(arguments[0])')
271
    assert False == run_with_sample_url('get_allowing(arguments[0])')
272

    
273
    run_with_sample_url('set_allowed(arguments[0])')
274
    assert True == run_with_sample_url('get_allowing(arguments[0])')
275

    
276
    run_with_sample_url('set_default_allowing(arguments[0])')
277
    assert None == run_with_sample_url('get_allowing(arguments[0])')
278

    
279
@pytest.mark.get_page('https://gotmyowndoma.in')
280
def test_haketilodb_repos(driver, execute_in_page):
281
    """
282
    indexeddb.js facilitates operating on Haketilo's internal database.
283
    Verify operations on repositories list work properly.
284
    """
285
    execute_in_page(load_script('common/indexeddb.js'))
286
    mock_broadcast(execute_in_page)
287

    
288
    # Start with no database.
289
    clear_indexeddb(execute_in_page)
290

    
291
    assert get_db_contents(execute_in_page)['repos'] == []
292

    
293
    sample_urls = ['https://hdrlla.example.com/', 'https://hdrlla.example.org']
294

    
295
    assert [] == execute_in_page('returnval(get_repos());')
296

    
297
    execute_in_page('returnval(set_repo(arguments[0]));', sample_urls[0])
298
    assert [sample_urls[0]] == execute_in_page('returnval(get_repos());')
299

    
300
    execute_in_page('returnval(set_repo(arguments[0]));', sample_urls[1])
301
    assert set(sample_urls) == set(execute_in_page('returnval(get_repos());'))
302

    
303
    execute_in_page('returnval(del_repo(arguments[0]));', sample_urls[0])
304
    assert [sample_urls[1]] == execute_in_page('returnval(get_repos());')
305

    
306
test_page_html = '''
307
<!DOCTYPE html>
308
<script src="/testpage.js"></script>
309
<body>
310
</body>
311
'''
312

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

    
334
    # Create elements that will have tracked data inserted under them.
335
    driver.switch_to.window(windows[0])
336
    execute_in_page('''
337
    for (const store_name of trackable) {
338
        const h2 = document.createElement("h2");
339
        h2.innerText = store_name;
340
        document.body.append(h2);
341

    
342
        const ul = document.createElement("ul");
343
        ul.id = store_name;
344
        document.body.append(ul);
345
    }
346
    ''')
347

    
348
    # Mock initial_data.
349
    sample_resource = make_sample_resource()
350
    sample_mapping = make_sample_mapping()
351
    initial_data = {
352
        'resources': {
353
            'helloapple': {
354
                '1.0': sample_resource
355
            }
356
        },
357
        'mappings': {
358
            'helloapple': {
359
                '0.1.1': sample_mapping
360
            }
361
        },
362
        'files': sample_files_by_hash
363
    }
364
    driver.switch_to.window(windows[1])
365
    execute_in_page('initial_data = arguments[0];', initial_data)
366
    execute_in_page('returnval(set_setting("option15", "123"));')
367
    execute_in_page('returnval(set_repo("https://hydril.la"));')
368
    execute_in_page('returnval(set_disallowed("file:///*"));')
369

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

    
386
        let resource_tracking, resource_items, mapping_tracking, mapping_items;
387

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

    
400
        returnval(start_reporting());
401
        ''')
402

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

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

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

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

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

    
483
        option_text = driver.find_element_by_id('settings_option22').text
484
        blocking_text = driver.find_element_by_id('blocking_ftp://a.bc/').text
485
        return (json.loads(option_text)['value'] == None and
486
                json.loads(blocking_text)['allow'] == False)
487

    
488
    driver.switch_to.window(windows[0])
489
    WebDriverWait(driver, 10).until(condition_items_absent_and_changed)
(8-8/22)