# SPDX-License-Identifier: CC0-1.0 """ Haketilo unit tests - IndexedDB access """ # This file is part of Haketilo # # Copyright (C) 2021, Wojtek Kosior # # 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 from hashlib import sha256 from ..script_loader import load_script @pytest.fixture(scope="session") def indexeddb_code(): yield load_script('common/indexeddb.js', ['common']) def sample_file(contents): return { 'hash_key': f'sha256-{sha256(contents.encode()).digest().hex()}', 'contents': contents } sample_files = { 'report.spdx': sample_file(''), 'LICENSES/somelicense.txt': sample_file('Permission is granted...'), 'hello.js': sample_file('console.log("hello!");\n'), 'bye.js': sample_file('console.log("bye!");\n'), 'combined.js': sample_file('console.log("hello!\\nbye!");\n'), 'README.md': sample_file('# Python Frobnicator\n...') } sample_files_by_hash = dict([[file['hash_key'], file['contents']] for file in sample_files.values()]) def file_ref(file_name): return {'file': file_name, 'hash_key': sample_files[file_name]['hash_key']} @pytest.mark.get_page('https://gotmyowndoma.in') def test_save_remove_item(execute_in_page, indexeddb_code): """ indexeddb.js facilitates operating on Haketilo's internal database. Verify database operations work properly. """ execute_in_page(indexeddb_code) # Don't use Haketilo's default initial data. execute_in_page('initial_data = {};') # Start with no database. execute_in_page( '''{ async function delete_db() { let resolve; const result = new Promise(_resolve => resolve = _resolve); const request = indexedDB.deleteDatabase("haketilo"); [request.onsuccess, request.onerror] = [resolve, resolve]; await result; } returnval(delete_db()); }''' ) # Facilitate retrieving all IndexedDB contents. execute_in_page( ''' async function get_database_contents() { const db = await haketilodb.get(); const transaction = db.transaction(db.objectStoreNames); const store_names_reqs = [...db.objectStoreNames] .map(sn => [sn, transaction.objectStore(sn).getAll()]) const promises = store_names_reqs .map(([_, req]) => wait_request(req)); await Promise.all(promises); const result = {}; store_names_reqs.forEach(([sn, req]) => result[sn] = req.result); return result; } ''') # Sample resource definition. It'd normally contain more fields but here # we use a simplified version. sample_item = { 'source_copyright': [ file_ref('report.spdx'), file_ref('LICENSES/somelicense.txt') ], 'type': 'resource', 'identifier': 'helloapple', 'scripts': [file_ref('hello.js'), file_ref('bye.js')], } next(iter(sample_item['source_copyright']))['ugly_extra_property'] = True database_contents = execute_in_page( '''{ const promise = start_items_transaction(["resources"], arguments[1]) .then(ctx => save_item(arguments[0], ctx).then(() => ctx)) .then(finalize_items_transaction) .then(get_database_contents); returnval(promise); }''', sample_item, sample_files_by_hash) assert len(database_contents['files']) == 4 assert all([sample_files_by_hash[file['hash_key']] == file['contents'] for file in database_contents['files']]) assert all([len(file) == 2 for file in database_contents['files']]) assert len(database_contents['file_uses']) == 4 assert all([uses['uses'] == 1 for uses in database_contents['file_uses']]) assert set([uses['hash_key'] for uses in database_contents['file_uses']]) \ == set([file['hash_key'] for file in database_contents['files']]) assert database_contents['mappings'] == [] assert database_contents['resources'] == [sample_item] # See if trying to add an item without providing all its files ends in an # exception and aborts the transaction as it should. sample_item['scripts'].append(file_ref('combined.js')) incomplete_files = {**sample_files_by_hash} incomplete_files.pop(sample_files['combined.js']['hash_key']) print ('incomplete files:', incomplete_files) print ('sample item:', sample_item) result = execute_in_page( '''{ console.log('sample item', arguments[0]); const promise = (async () => { const context = await start_items_transaction(["resources"], arguments[1]); try { await save_item(arguments[0], context); await finalize_items_transaction(context); return {}; } catch(e) { var exception = e; } return {exception, db_contents: await get_database_contents()}; })(); returnval(promise); }''', sample_item, incomplete_files) assert result assert 'file not present' in result['exception'] for key, val in database_contents.items(): keyfun = lambda item: item.get('hash_key') or item['identifier'] assert sorted(result['db_contents'][key], key=keyfun) \ == sorted(val, key=keyfun) # See if adding another item that partially uses first's files works OK. sample_item = { 'source_copyright': [ file_ref('report.spdx'), file_ref('README.md') ], 'type': 'mapping', 'identifier': 'helloapple', } database_contents = execute_in_page( '''{ const promise = start_items_transaction(["mappings"], arguments[1]) .then(ctx => save_item(arguments[0], ctx).then(() => ctx)) .then(finalize_items_transaction) .then(get_database_contents); returnval(promise); }''', sample_item, sample_files_by_hash) names = ['README.md', 'report.spdx', 'LICENSES/somelicense.txt', 'hello.js', 'bye.js'] sample_files_list = [sample_files[name] for name in names] uses_list = [1, 2, 1, 1, 1] uses = dict([(uses['hash_key'], uses['uses']) for uses in database_contents['file_uses']]) assert uses == dict([(file['hash_key'], nr) for file, nr in zip(sample_files_list, uses_list)]) files = dict([(file['hash_key'], file['contents']) for file in database_contents['files']]) assert files == dict([(file['hash_key'], file['contents']) for file in sample_files_list]) assert database_contents['mappings'] == [sample_item] # Try removing the items to get an empty database again. results = [None, None] for i, item_type in enumerate(['resource', 'mapping']): results[i] = execute_in_page( f'''{{ const remover = remove_{item_type}; const promise = start_items_transaction(["{item_type}s"], {{}}) .then(ctx => remover('helloapple', ctx).then(() => ctx)) .then(finalize_items_transaction) .then(get_database_contents); returnval(promise); }}''') names = ['README.md', 'report.spdx'] sample_files_list = [sample_files[name] for name in names] uses_list = [1, 1] uses = dict([(uses['hash_key'], uses['uses']) for uses in results[0]['file_uses']]) assert uses == dict([(file['hash_key'], 1) for file in sample_files_list]) files = dict([(file['hash_key'], file['contents']) for file in results[0]['files']]) assert files == dict([(file['hash_key'], file['contents']) for file in sample_files_list]) assert results[0]['resources'] == [] assert results[0]['mappings'] == [sample_item] assert results[1] == dict([(key, []) for key in results[0].keys()])