# 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?')