Project

General

Profile

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

haketilo / test / haketilo_test / unit / test_install.py @ f8dedf60

1
# SPDX-License-Identifier: CC0-1.0
2

    
3
"""
4
Haketilo unit tests - item installation dialog
5
"""
6

    
7
# This file is part of Haketilo
8
#
9
# Copyright (C) 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.support.ui import WebDriverWait
23

    
24
from ..extension_crafting import ExtraHTML
25
from ..script_loader import load_script
26
from .utils import *
27

    
28
def setup_view(driver, execute_in_page):
29
    execute_in_page(mock_cacher_code)
30

    
31
    execute_in_page(load_script('html/install.js'))
32
    container_ids, containers_objects = execute_in_page(
33
        '''
34
        const cb_calls = [];
35
        const install_view = new InstallView(0,
36
                                             () => cb_calls.push("show"),
37
                                             () => cb_calls.push("hide"));
38
        document.body.append(install_view.main_div);
39
        const ets = () => install_view.item_entries;
40
        const shw = slice => [cb_calls.slice(slice || 0), install_view.shown];
41
        returnval([container_ids, container_ids.map(cid => install_view[cid])]);
42
        ''')
43

    
44
    containers = dict(zip(container_ids, containers_objects))
45

    
46
    def assert_container_displayed(container_id):
47
        for cid, cobj in zip(container_ids, containers_objects):
48
            assert (cid == container_id) == cobj.is_displayed()
49

    
50
    return containers, assert_container_displayed
51

    
52
install_ext_data = {
53
    'background_script': broker_js,
54
    'extra_html': ExtraHTML('html/install.html', {}),
55
    'navigate_to': 'html/install.html'
56
}
57

    
58
@pytest.mark.ext_data(install_ext_data)
59
@pytest.mark.usefixtures('webextension')
60
@pytest.mark.parametrize('variant', [{
61
    # The resource/mapping others depend on.
62
    'root_resource_id': f'resource-abcd-defg-ghij',
63
    'root_mapping_id':  f'mapping-abcd-defg-ghij',
64
    # Those ids are used to check the alphabetical ordering.
65
    'item_ids': [f'resource-{letters}' for letters in (
66
        'a', 'abcd', 'abcd-defg-ghij', 'b', 'c',
67
        'd', 'defg', 'e', 'f',
68
        'g', 'ghij', 'h', 'i', 'j'
69
    )],
70
    'files_count': 9
71
}, {
72
    'root_resource_id': 'resource-a',
73
    'root_mapping_id':  'mapping-a',
74
    'item_ids':         ['resource-a'],
75
    'files_count': 0
76
}, {
77
    'root_resource_id': 'resource-a-w-required-mapping-v1',
78
    'root_mapping_id':  'mapping-a-w-required-mapping-v1',
79
    'item_ids':         ['resource-a-w-required-mapping-v1'],
80
    'files_count':      1
81
}, {
82
    'root_resource_id': 'resource-a-w-required-mapping-v2',
83
    'root_mapping_id':  'mapping-a-w-required-mapping-v2',
84
    'item_ids':         [
85
        'mapping-a',
86
        'resource-a',
87
        'resource-a-w-required-mapping-v2'
88
    ],
89
    'files_count':      1
90
}])
91
def test_install_normal_usage(driver, execute_in_page, variant):
92
    """
93
    Test of the normal package installation procedure with one mapping and,
94
    depending on parameter, one or many resources.
95
    """
96
    containers, assert_container_displayed = setup_view(driver, execute_in_page)
97

    
98
    assert execute_in_page('returnval(shw());') == [[], False]
99

    
100
    # Preview the installation of a resource, show resource's details, close
101
    # the details and cancel installation.
102
    execute_in_page('returnval(install_view.show(...arguments));',
103
                    'https://hydril.la/', 'resource',
104
                    variant['root_resource_id'])
105

    
106
    assert execute_in_page('returnval(shw());') == [['show'], True]
107
    assert f'{variant["root_resource_id"]}-2021.11.11-1'\
108
        in containers['install_preview'].text
109
    assert_container_displayed('install_preview')
110

    
111
    entries = execute_in_page('returnval(ets().map(e => e.main_li.innerText));')
112
    assert len(entries) == len(variant['item_ids'])
113
    resource_idx = variant['item_ids'].index(variant['root_resource_id'])
114
    # Verify alphabetical ordering.
115
    assert all([id in text for id, text in
116
                zip(variant['item_ids'], entries)])
117

    
118
    assert not execute_in_page(f'returnval(ets()[{resource_idx}].old_ver);')\
119
        .is_displayed()
120
    execute_in_page(f'returnval(ets()[{resource_idx}].details_but);').click()
121
    assert 'resource-a' in containers['resource_preview_container'].text
122
    assert_container_displayed('resource_preview_container')
123

    
124
    execute_in_page('returnval(install_view.resource_back_but);').click()
125
    assert_container_displayed('install_preview')
126

    
127
    assert execute_in_page('returnval(shw());') == [['show'], True]
128
    execute_in_page('returnval(install_view.cancel_but);').click()
129
    assert execute_in_page('returnval(shw());') == [['show', 'hide'], False]
130

    
131
    # Preview the installation of a mapping and a resource, show mapping's
132
    # details, close the details and commit the installation.
133
    execute_in_page('returnval(install_view.show(...arguments));',
134
                    'https://hydril.la/', 'mapping',
135
                    variant['root_mapping_id'], [2022, 5, 10])
136

    
137
    assert execute_in_page('returnval(shw(2));') == [['show'], True]
138
    assert_container_displayed('install_preview')
139

    
140
    entries = execute_in_page('returnval(ets().map(e => e.main_li.innerText));')
141
    assert len(entries) == len(variant['item_ids']) + 1
142

    
143
    all_item_ids = sorted([*variant['item_ids'], variant['root_mapping_id']])
144
    mapping_idx = all_item_ids.index(variant["root_mapping_id"])
145
    # Verify alphabetical ordering.
146
    assert all([id in text for id, text in zip(all_item_ids, entries)])
147

    
148
    assert not execute_in_page(f'returnval(ets()[{mapping_idx}].old_ver);')\
149
        .is_displayed()
150
    execute_in_page(f'returnval(ets()[{mapping_idx}].details_but);').click()
151
    assert variant['root_mapping_id'] in \
152
        containers['mapping_preview_container'].text
153
    assert_container_displayed('mapping_preview_container')
154

    
155
    execute_in_page('returnval(install_view.mapping_back_but);').click()
156
    assert_container_displayed('install_preview')
157

    
158
    execute_in_page('returnval(install_view.install_but);').click()
159
    installed = lambda d: 'ly installed!' in containers['dialog_container'].text
160
    WebDriverWait(driver, 10).until(installed)
161

    
162
    assert execute_in_page('returnval(shw(2));') == [['show'], True]
163
    execute_in_page('returnval(install_view.dialog_ctx.ok_but);').click()
164
    assert execute_in_page('returnval(shw(2));') == [['show', 'hide'], False]
165

    
166
    # Verify the install
167
    db_contents = get_db_contents(execute_in_page)
168
    all_map_ids = {id for id in all_item_ids if id.startswith('mapping')}
169
    all_res_ids = {id for id in all_item_ids if id.startswith('resource')}
170
    for item_type, ids in [
171
            ('mapping', all_map_ids),
172
            ('resource', all_res_ids)
173
    ]:
174
        assert set([it['identifier'] for it in db_contents[item_type]]) == ids
175

    
176
    assert all([len(db_contents[store]) == variant['files_count']
177
                for store in ('file', 'file_uses')])
178

    
179
    # Update the installed mapping to a newer version.
180
    execute_in_page('returnval(install_view.show(...arguments));',
181
                    'https://hydril.la/', 'mapping', variant['root_mapping_id'])
182
    assert execute_in_page('returnval(shw(4));') == [['show'], True]
183
    # resources are already in the newest versions, hence they should not appear
184
    # in the install preview list.
185
    assert execute_in_page('returnval(ets().length);') == 1
186
    # Mapping's version update information should be displayed.
187
    assert execute_in_page('returnval(ets()[0].old_ver);').is_displayed()
188
    execute_in_page('returnval(install_view.install_but);').click()
189

    
190
    WebDriverWait(driver, 10).until(installed)
191

    
192
    assert execute_in_page('returnval(shw(4));') == [['show'], True]
193
    execute_in_page('returnval(install_view.dialog_ctx.ok_but);').click()
194
    assert execute_in_page('returnval(shw(4));') == [['show', 'hide'], False]
195

    
196
    # Verify the newer version install.
197
    old_db_contents, db_contents = db_contents, get_db_contents(execute_in_page)
198

    
199
    old_root_mapping = [m for m in old_db_contents['mapping']
200
                        if m['identifier'] == variant['root_mapping_id']][0]
201
    old_root_mapping['version'][-1] += 1
202

    
203
    new_root_mapping = [m for m in db_contents['mapping']
204
                        if m['identifier'] == variant['root_mapping_id']][0]
205

    
206
    assert old_root_mapping == new_root_mapping
207

    
208
    # All items are up to date - verify dialog is instead shown in this case.
209
    execute_in_page('install_view.show(...arguments);',
210
                    'https://hydril.la/', 'mapping', variant['root_mapping_id'])
211

    
212
    fetched = lambda d: 'Fetching ' not in containers['dialog_container'].text
213
    WebDriverWait(driver, 10).until(fetched)
214

    
215
    assert 'Nothing to do - packages already installed.' \
216
        in containers['dialog_container'].text
217
    assert_container_displayed('dialog_container')
218

    
219
    assert execute_in_page('returnval(shw(6));') == [['show'], True]
220
    execute_in_page('returnval(install_view.dialog_ctx.ok_but);').click()
221
    assert execute_in_page('returnval(shw(6));') == [['show', 'hide'], False]
222

    
223
@pytest.mark.ext_data(install_ext_data)
224
@pytest.mark.usefixtures('webextension')
225
@pytest.mark.parametrize('message', [
226
    'fetching_data',
227
    'failure_to_communicate_sendmessage',
228
    'HTTP_code_item',
229
    'invalid_JSON',
230
    'newer_API_version',
231
    'invalid_response_format',
232
    'indexeddb_error_item',
233
    'installing',
234
    'indexeddb_error_file_uses',
235
    'failure_to_communicate_fetch',
236
    'HTTP_code_file',
237
    'sha256_mismatch',
238
    'indexeddb_error_write'
239
])
240
def test_install_dialogs(driver, execute_in_page, message):
241
    """
242
    Test of various error and loading messages used in install view.
243
    """
244
    containers, assert_container_displayed = setup_view(driver, execute_in_page)
245

    
246
    def dlg_buts():
247
        return execute_in_page(
248
            '''{
249
            const dlg = install_view.dialog_ctx;
250
            const ids = ['ask_buts', 'conf_buts'];
251
            returnval(ids.filter(id => !dlg[id].classList.contains("hide")));
252
            }''')
253

    
254
    def dialog_txt():
255
        return execute_in_page(
256
            'returnval(install_view.dialog_ctx.msg.textContent);'
257
        )
258

    
259
    def assert_dlg(awaited_buttons, expected_msg, hides_install_view=True,
260
                   button_to_click='ok_but'):
261
        WebDriverWait(driver, 10).until(lambda d: dlg_buts() == awaited_buttons)
262

    
263
        assert expected_msg == dialog_txt()
264

    
265
        execute_in_page(
266
            f'returnval(install_view.dialog_ctx.{button_to_click});'
267
        ).click()
268

    
269
        if hides_install_view:
270
            assert execute_in_page('returnval(shw());') == \
271
                [['show', 'hide'], False]
272

    
273
    if message == 'fetching_data':
274
        execute_in_page(
275
            '''
276
            window.mock_cacher_fetch = () => new Promise(cb => {});
277
            install_view.show(...arguments);
278
            ''',
279
            'https://hydril.la/', 'mapping', 'mapping-a')
280

    
281
        assert dlg_buts() == []
282
        assert dialog_txt() == 'Fetching data from repository...'
283
    elif message == 'failure_to_communicate_sendmessage':
284
        execute_in_page(
285
            '''
286
            window.mock_cacher_fetch =
287
                () => {throw new Error("Something happened :o")};
288
            install_view.show(...arguments);
289
            ''',
290
            'https://hydril.la/', 'mapping', 'mapping-a')
291

    
292
        assert_dlg(['conf_buts'], 'Failure to communicate with repository :(')
293
    elif message == 'HTTP_code_item':
294
        execute_in_page(
295
            '''
296
            const response = new Response("", {status: 404});
297
            window.mock_cacher_fetch = () => Promise.resolve(response);
298
            install_view.show(...arguments);
299
            ''',
300
            'https://hydril.la/', 'mapping', 'mapping-a')
301

    
302
        assert_dlg(['conf_buts'], 'Repository sent HTTP code 404 :(')
303
    elif message == 'invalid_JSON':
304
        execute_in_page(
305
            '''
306
            const response = new Response("sth", {status: 200});
307
            window.mock_cacher_fetch = () => Promise.resolve(response);
308
            install_view.show(...arguments);
309
            ''',
310
            'https://hydril.la/', 'mapping', 'mapping-a')
311

    
312
        assert_dlg(['conf_buts'], "Repository's response is not valid JSON :(")
313
    elif message == 'newer_API_version':
314
        execute_in_page(
315
            '''
316
            const newer_schema_url =
317
                "https://hydrilla.koszko.org/schemas/api_mapping_description-255.1.schema.json";
318
            const mocked_json_data = JSON.stringify({$schema: newer_schema_url});
319
            const response = new Response(mocked_json_data, {status: 200});
320
            window.mock_cacher_fetch = () => Promise.resolve(response);
321
            install_view.show(...arguments);
322
            ''',
323
            'https://hydril.la/', 'mapping', 'mapping-a', [2022, 5, 10])
324

    
325
        assert_dlg(['conf_buts'],
326
                   'Mapping mapping-a-2022.5.10 was served using unsupported Hydrilla API version. You might need to update Haketilo.')
327
    elif message == 'invalid_response_format':
328
        execute_in_page(
329
            '''
330
            window.mock_cacher_fetch = async function(...args) {
331
                const response = await fetch(...args);
332
                const json = await response.json();
333

    
334
                /* identifier is no longer a string as it should be. */
335
                json.identifier = 1234567;
336

    
337
                return new Response(JSON.stringify(json), {
338
                    status:     response.status,
339
                    statusText: response.statusText,
340
                    headers:    [...response.headers.entries()]
341
                });
342
            }
343
            install_view.show(...arguments);
344
            ''',
345
            'https://hydril.la/', 'resource', 'resource-a')
346

    
347
        assert_dlg(['conf_buts'],
348
                   'Resource resource-a was served using a nonconforming response format.')
349
    elif message == 'indexeddb_error_item':
350
        execute_in_page(
351
            '''
352
            haketilodb.idb_get = () => {throw "some error";};
353
            install_view.show(...arguments);
354
            ''',
355
            'https://hydril.la/', 'mapping', 'mapping-a')
356

    
357
        assert_dlg(['conf_buts'],
358
                   "Error accessing Haketilo's internal database :(")
359
    elif message == 'installing':
360
        execute_in_page(
361
            '''
362
            haketilodb.save_items = () => new Promise(() => {});
363
            returnval(install_view.show(...arguments));
364
            ''',
365
            'https://hydril.la/', 'mapping', 'mapping-b')
366

    
367
        execute_in_page('returnval(install_view.install_but);').click()
368

    
369
        assert dlg_buts() == []
370
        assert dialog_txt() == 'Installing...'
371
    elif message == 'indexeddb_error_file_uses':
372
        execute_in_page(
373
            '''
374
            const old_idb_get = haketilodb.idb_get;
375
            haketilodb.idb_get = function(transaction, store_name, identifier) {
376
                if (store_name === "file_uses")
377
                    throw "some error";
378
                return old_idb_get(...arguments);
379
            }
380
            returnval(install_view.show(...arguments));
381
            ''',
382
            'https://hydril.la/', 'mapping', 'mapping-b')
383

    
384
        execute_in_page('returnval(install_view.install_but);').click()
385

    
386
        assert_dlg(['conf_buts'],
387
                   "Error accessing Haketilo's internal database :(")
388
    elif message == 'failure_to_communicate_fetch':
389
        execute_in_page(
390
            '''
391
            fetch = () => {throw new Error("some error");};
392
            returnval(install_view.show(...arguments));
393
            ''',
394
            'https://hydril.la/', 'mapping', 'mapping-b')
395

    
396
        execute_in_page('returnval(install_view.install_but);').click()
397

    
398
        assert_dlg(['conf_buts'],
399
                   'Failure to communicate with repository :(')
400
    elif message == 'HTTP_code_file':
401
        execute_in_page(
402
            '''
403
            fetch = () => Promise.resolve({ok: false, status: 400});
404
            returnval(install_view.show(...arguments));
405
            ''',
406
            'https://hydril.la/', 'mapping', 'mapping-b')
407

    
408
        execute_in_page('returnval(install_view.install_but);').click()
409

    
410
        assert_dlg(['conf_buts'], 'Repository sent HTTP code 400 :(')
411
    elif message == 'sha256_mismatch':
412
        execute_in_page(
413
            '''
414
            let old_fetch = fetch, url_used;
415
            fetch = async function(url) {
416
                url_used = url;
417
                const response = await old_fetch(...arguments);
418
                const text = () => response.text().then(t => t + ":d");
419
                return {ok: response.ok, status: response.status, text};
420
            }
421
            returnval(install_view.show(...arguments));
422
            ''',
423
            'https://hydril.la/', 'mapping', 'mapping-b')
424

    
425
        execute_in_page('returnval(install_view.install_but);').click()
426

    
427
        get_url_used = lambda d: execute_in_page('returnval(url_used);')
428
        url_used = WebDriverWait(driver, 10).until(get_url_used)
429
        print ((url_used,))
430

    
431
        assert dlg_buts() == ['conf_buts']
432
        assert dialog_txt() == \
433
            f'{url_used} served a file with different SHA256 cryptographic sum :('
434
    elif message == 'indexeddb_error_write':
435
        execute_in_page(
436
            '''
437
            haketilodb.save_items = () => {throw "some error";};
438
            returnval(install_view.show(...arguments));
439
            ''',
440
            'https://hydril.la/', 'mapping', 'mapping-b')
441

    
442
        execute_in_page('returnval(install_view.install_but);').click()
443

    
444
        assert_dlg(['conf_buts'],
445
                   "Error writing to Haketilo's internal database :(")
446
    else:
447
        raise Exception('made a typo in test function params?')
(11-11/26)