Project

General

Profile

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

haketilo / test / haketilo_test / unit / test_indexeddb.py @ 587c1a88

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_item_modifications(driver, execute_in_page):
55
    """
56
    indexeddb.js facilitates operating on Haketilo's internal database.
57
    Verify database operations on mappings/resources work properly.
58
    """
59
    execute_in_page(load_script('common/indexeddb.js'))
60
    mock_broadcast(execute_in_page)
61

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

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

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

    
77
    database_contents = get_db_contents(execute_in_page)
78

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

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

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

    
92
    # See if trying to add an item without providing all its files ends in an
93
    # exception and aborts the transaction as it should.
94
    sample_item['scripts'].append(sample_file_ref('combined.js'))
95
    incomplete_files = {**sample_files_by_sha256}
96
    incomplete_files.pop(sample_files['combined.js']['sha256'])
97
    exception = execute_in_page(
98
        '''{
99
        const args = arguments;
100
        async function try_add_item()
101
        {
102
            const context =
103
                await start_items_transaction(["resource"], args[1]);
104
            try {
105
                await save_item(args[0], context);
106
                await finalize_transaction(context);
107
                return;
108
            } catch(e) {
109
                return e;
110
            }
111
        }
112
        returnval(try_add_item());
113
        }''',
114
        sample_item, {'sha256': incomplete_files})
115

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

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

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

    
136
    database_contents = get_db_contents(execute_in_page)
137

    
138
    names = ['README.md', 'report.spdx', 'LICENSES/somelicense.txt', 'hello.js',
139
             'bye.js']
140
    sample_files_list = [sample_files[name] for name in names]
141
    uses_list = [1, 2, 1, 1, 1]
142

    
143
    uses = dict([(uses['sha256'], uses['uses'])
144
                 for uses in database_contents['file_uses']])
145
    assert uses  == dict([(file['sha256'], nr)
146
                          for file, nr in zip(sample_files_list, uses_list)])
147

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

    
153
    del database_contents['resource'][0]['source_copyright'][0]['extra_prop']
154
    assert database_contents['resource'] == [make_sample_resource()]
155
    assert database_contents['mapping']  == [sample_item]
156

    
157
    # Try removing the items to get an empty database again.
158
    results = [None, None]
159
    for i, item_type in enumerate(['resource', 'mapping']):
160
         execute_in_page(
161
            f'''{{
162
            const remover = remove_{item_type};
163
            const promise =
164
                start_items_transaction(["{item_type}"], {{}})
165
                .then(ctx => remover('helloapple', ctx).then(() => ctx))
166
                .then(finalize_transaction);
167
            returnval(promise);
168
            }}''')
169

    
170
         results[i] = get_db_contents(execute_in_page)
171

    
172
    names = ['README.md', 'report.spdx']
173
    sample_files_list = [sample_files[name] for name in names]
174
    uses_list = [1, 1]
175

    
176
    uses = dict([(uses['sha256'], uses['uses'])
177
                 for uses in results[0]['file_uses']])
178
    assert uses  == dict([(file['sha256'], 1) for file in sample_files_list])
179

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

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

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

    
190
    # Try initializing an empty database with sample initial data object.
191
    sample_resource = make_sample_resource()
192
    sample_mapping = make_sample_mapping()
193
    initial_data = {
194
        'resource': {
195
            'helloapple': {
196
                '1.12':   sample_resource,
197
                '0.9':    'something_that_should_get_ignored',
198
                '1':      'something_that_should_get_ignored',
199
                '1.1':    'something_that_should_get_ignored',
200
                '1.11.1': 'something_that_should_get_ignored',
201
            }
202
        },
203
        'mapping': {
204
            'helloapple': {
205
                '0.1.1': sample_mapping
206
            }
207
        },
208
        'file': {
209
            'sha256': sample_files_by_sha256
210
        }
211
    }
212

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
380
        let resource_tracking, resource_items, mapping_tracking, mapping_items;
381

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

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

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

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

    
441
    driver.switch_to.window(windows[0])
442
    driver.implicitly_wait(10)
443
    for elem_id, json_value in [
444
            ('resource_helloapple-copy', sample_resource2),
445
            ('mapping_helloapple-copy', sample_mapping2),
446
            ('setting_option22', {'name': 'option22', 'value': 'abc'}),
447
            ('repo_https://hydril2.la/', {'url': 'https://hydril2.la/'}),
448
            ('repo_https://hydril3.la/', {'url': 'https://hydril3.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
                   'repo_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('setting_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/25)