aboutsummaryrefslogtreecommitdiff
# SPDX-License-Identifier: CC0-1.0

"""
Haketilo unit tests - item installation dialog
"""

# This file is part of Haketilo
#
# Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the CC0 1.0 Universal License as published by
# the Creative Commons Corporation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# CC0 1.0 Universal License for more details.

import pytest
import json
from selenium.webdriver.support.ui import WebDriverWait

from ..extension_crafting import ExtraHTML
from ..script_loader import load_script
from .utils import *

def setup_view(driver, execute_in_page):
    execute_in_page(mock_cacher_code)

    execute_in_page(load_script('html/install.js'))
    container_ids, containers_objects = execute_in_page(
        '''
        const cb_calls = [];
        const install_view = new InstallView(0,
                                             () => cb_calls.push("show"),
                                             () => cb_calls.push("hide"));
        document.body.append(install_view.main_div);
        const ets = () => install_view.item_entries;
        const shw = slice => [cb_calls.slice(slice || 0), install_view.shown];
        returnval([container_ids, container_ids.map(cid => install_view[cid])]);
        ''')

    containers = dict(zip(container_ids, containers_objects))

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

    return containers, assert_container_displayed

install_ext_data = {
    'background_script': broker_js,
    'extra_html': ExtraHTML('html/install.html', {}),
    'navigate_to': 'html/install.html'
}

@pytest.mark.ext_data(install_ext_data)
@pytest.mark.usefixtures('webextension')
@pytest.mark.parametrize('variant', [{
    # The resource/mapping others depend on.
    'root_resource_id': f'resource-abcd-defg-ghij',
    'root_mapping_id':  f'mapping-abcd-defg-ghij',
    # Those ids are used to check the alphabetical ordering.
    'item_ids': [f'resource-{letters}' for letters in (
        'a', 'abcd', 'abcd-defg-ghij', 'b', 'c',
        'd', 'defg', 'e', 'f',
        'g', 'ghij', 'h', 'i', 'j'
    )],
    'files_count': 9
}, {
    'root_resource_id': 'resource-a',
    'root_mapping_id':  'mapping-a',
    'item_ids':         ['resource-a'],
    'files_count': 0
}, {
    'root_resource_id': 'resource-a-w-required-mapping-v1',
    'root_mapping_id':  'mapping-a-w-required-mapping-v1',
    'item_ids':         ['resource-a-w-required-mapping-v1'],
    'files_count':      1
}, {
    'root_resource_id': 'resource-a-w-required-mapping-v2',
    'root_mapping_id':  'mapping-a-w-required-mapping-v2',
    'item_ids':         [
        'mapping-a',
        'resource-a',
        'resource-a-w-required-mapping-v2'
    ],
    'files_count':      1
}])
def test_install_normal_usage(driver, execute_in_page, variant):
    """
    Test of the normal package installation procedure with one mapping and,
    depending on parameter, one or many resources.
    """
    containers, assert_container_displayed = setup_view(driver, execute_in_page)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    # Verify the install
    db_contents = get_db_contents(execute_in_page)
    all_map_ids = {id for id in all_item_ids if id.startswith('mapping')}
    all_res_ids = {id for id in all_item_ids if id.startswith('resource')}
    for item_type, ids in [
            ('mapping', all_map_ids),
            ('resource', all_res_ids)
    ]:
        assert set([it['identifier'] for it in db_contents[item_type]]) == ids

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

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

    WebDriverWait(driver, 10).until(installed)

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

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

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

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

    assert old_root_mapping == new_root_mapping

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

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

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

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

@pytest.mark.ext_data(install_ext_data)
@pytest.mark.usefixtures('webextension')
@pytest.mark.parametrize('message', [
    'fetching_data',
    'failure_to_communicate_sendmessage',
    'HTTP_code_item',
    'invalid_JSON',
    'newer_API_version',
    'invalid_response_format',
    'indexeddb_error_item',
    'installing',
    'indexeddb_error_file_uses',
    'failure_to_communicate_fetch',
    'HTTP_code_file',
    'sha256_mismatch',
    'indexeddb_error_write'
])
def test_install_dialogs(driver, execute_in_page, message):
    """
    Test of various error and loading messages used in install view.
    """
    containers, assert_container_displayed = setup_view(driver, execute_in_page)

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

    def dialog_txt():
        return execute_in_page(
            'returnval(install_view.dialog_ctx.msg.textContent);'
        )

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

        assert expected_msg == dialog_txt()

        execute_in_page(
            f'returnval(install_view.dialog_ctx.{button_to_click});'
        ).click()

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

    if message == 'fetching_data':
        execute_in_page(
            '''
            window.mock_cacher_fetch = () => new Promise(cb => {});
            install_view.show(...arguments);
            ''',
            'https://hydril.la/', 'mapping', 'mapping-a')

        assert dlg_buts() == []
        assert dialog_txt() == 'Fetching data from repository...'
    elif message == 'failure_to_communicate_sendmessage':
        execute_in_page(
            '''
            window.mock_cacher_fetch =
                () => {throw new Error("Something happened :o")};
            install_view.show(...arguments);
            ''',
            'https://hydril.la/', 'mapping', 'mapping-a')

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

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

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

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

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

                return new Response(JSON.stringify(json), {
                    status:     response.status,
                    statusText: response.statusText,
                    headers:    [...response.headers.entries()]
                });
            }
            install_view.show(...arguments);
            ''',
            'https://hydril.la/', 'resource', 'resource-a')

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

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

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

        assert dlg_buts() == []
        assert dialog_txt() == 'Installing...'
    elif message == 'indexeddb_error_file_uses':
        execute_in_page(
            '''
            const old_idb_get = haketilodb.idb_get;
            haketilodb.idb_get = function(transaction, store_name, identifier) {
                if (store_name === "file_uses")
                    throw "some error";
                return old_idb_get(...arguments);
            }
            returnval(install_view.show(...arguments));
            ''',
            'https://hydril.la/', 'mapping', 'mapping-b')

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

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

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

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

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

        assert_dlg(['conf_buts'], 'Repository sent HTTP code 400 :(')
    elif message == 'sha256_mismatch':
        execute_in_page(
            '''
            let old_fetch = fetch, url_used;
            fetch = async function(url) {
                url_used = url;
                const response = await old_fetch(...arguments);
                const text = () => response.text().then(t => t + ":d");
                return {ok: response.ok, status: response.status, text};
            }
            returnval(install_view.show(...arguments));
            ''',
            'https://hydril.la/', 'mapping', 'mapping-b')

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

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

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

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

        assert_dlg(['conf_buts'],
                   "Error writing to Haketilo's internal database :(")
    else:
        raise Exception('made a typo in test function params?')