From 0feb9db2b57725d47a7b3cc1e84ba5b9a7553b12 Mon Sep 17 00:00:00 2001 From: Wojtek Kosior Date: Mon, 3 Jan 2022 12:11:23 +0100 Subject: add "blocking" and "repos" object stores --- common/indexeddb.js | 81 ++++++++++++++++++--- test/unit/conftest.py | 3 +- test/unit/test_indexeddb.py | 169 +++++++++++++++++++++++++++++++------------- 3 files changed, 193 insertions(+), 60 deletions(-) diff --git a/common/indexeddb.js b/common/indexeddb.js index e54d1ca..8a30ce4 100644 --- a/common/indexeddb.js +++ b/common/indexeddb.js @@ -63,7 +63,9 @@ const stores = [ ["file_uses", {keyPath: "hash_key"}], ["resources", {keyPath: "identifier"}], ["mappings", {keyPath: "identifier"}], - ["settings", {keyPath: "name"}] + ["settings", {keyPath: "name"}], + ["blocking", {keyPath: "pattern"}], + ["repos", {keyPath: "url"}] ]; let db = null; @@ -364,17 +366,31 @@ const remove_resource = (id, ctx) => remove_item("resources", id, ctx); const remove_mapping = (id, ctx) => remove_item("mappings", id, ctx); #EXPORT remove_mapping -/* A simplified kind of transaction for modifying just the "settings" store. */ -async function start_settings_transaction() +/* Function to retrieve all items from a given store. */ +async function get_all(store_name) +{ + const transaction = (await get_db()).transaction([store_name]); + const all_req = transaction.objectStore(store_name).getAll(); + + return (await wait_request(all_req)).target.result; +} +#EXPORT get_all + +/* + * A simplified kind of transaction for modifying stores without special + * inter-store integrity constraints ("settings", "blocking", "repos"). + */ +async function start_simple_transaction(store_name) { const db = await get_db(); - return make_context(db.transaction("settings", "readwrite"), {}); + return make_context(db.transaction(store_name, "readwrite"), {}); } +/* Functions to access the "settings" store. */ async function set_setting(name, value) { - const context = await start_settings_transaction(); - broadcast.prepare(context.sender, `idb_changes_settings`, name); + const context = await start_simple_transaction("settings"); + broadcast.prepare(context.sender, "idb_changes_settings", name); await idb_put(context.transaction, "settings", {name, value}); return finalize_transaction(context); } @@ -387,6 +403,51 @@ async function get_setting(name) } #EXPORT get_setting +/* Functions to access the "blocking" store. */ +async function set_allowed(pattern, allow=true) +{ + const context = await start_simple_transaction("blocking"); + broadcast.prepare(context.sender, "idb_changes_blocking", pattern); + if (allow === null) + await idb_del(context.transaction, "blocking", pattern); + else + await idb_put(context.transaction, "blocking", {pattern, allow}); + return finalize_transaction(context); +} +#EXPORT set_allowed + +const set_disallowed = pattern => set_allowed(pattern, false); +#EXPORT set_disallowed + +const set_default_allowing = pattern => set_allowed(pattern, null); +#EXPORT set_default_allowing + +async function get_allowing(pattern) +{ + const transaction = (await get_db()).transaction("blocking"); + return ((await idb_get(transaction, "blocking", pattern)) || {}).allow; +} +#EXPORT get_allowing + +/* Functions to access the "repos" store. */ +async function set_repo(url, remove=false) +{ + const context = await start_simple_transaction("repos"); + broadcast.prepare(context.sender, "idb_changes_repos", url); + if (remove) + await idb_del(context.transaction, "repos", url); + else + await idb_put(context.transaction, "repos", {url}); + return finalize_transaction(context); +} +#EXPORT set_repo + +const del_repo = url => set_repo(url, true); +#EXPORT del_repo + +const get_repos = () => get_all("repos").then(list => list.map(obj => obj.url)); +#EXPORT get_repos + /* Callback used when listening to broadcasts while tracking db changes. */ async function track_change(tracking, key) { @@ -426,14 +487,12 @@ async function start_tracking(store_name, onchange) broadcast.listener_connection(msg => track_change(tracking, msg[1])); broadcast.subscribe(tracking.listener, `idb_changes_${store_name}`); - const transaction = (await get_db()).transaction([store_name]); - const all_req = transaction.objectStore(store_name).getAll(); - - return [tracking, (await wait_request(all_req)).target.result]; + return [tracking, await get_all(store_name)]; } const track = {}; -for (const store_name of ["resources", "mappings", "settings"]) +const trackable = ["resources", "mappings", "settings", "blocking", "repos"]; +for (const store_name of trackable) track[store_name] = onchange => start_tracking(store_name, onchange); #EXPORT track diff --git a/test/unit/conftest.py b/test/unit/conftest.py index beffaf5..e7be339 100644 --- a/test/unit/conftest.py +++ b/test/unit/conftest.py @@ -122,7 +122,8 @@ def _execute_in_page_context(driver, script, args): try: result = driver.execute_script(script_injector_script, script_url, args) - if type(result) == list and result[0] == 'haketilo_selenium_error': + if type(result) is list and len(result) == 2 and \ + result[0] == 'haketilo_selenium_error': raise Exception(result[1]) return result except Exception as e: diff --git a/test/unit/test_indexeddb.py b/test/unit/test_indexeddb.py index df3df81..447ee6e 100644 --- a/test/unit/test_indexeddb.py +++ b/test/unit/test_indexeddb.py @@ -296,8 +296,7 @@ def test_haketilodb_item_modifications(driver, execute_in_page): def test_haketilodb_settings(driver, execute_in_page): """ indexeddb.js facilitates operating on Haketilo's internal database. - Verify database assigning/retrieving values of simple "settings" works - properly. + Verify assigning/retrieving values of simple "settings" item works properly. """ execute_in_page(indexeddb_js()) mock_broadcast(execute_in_page) @@ -315,17 +314,66 @@ def test_haketilodb_settings(driver, execute_in_page): execute_in_page('returnval(set_setting("option15", "enable"));') assert execute_in_page('returnval(get_setting("option15"));') == 'enable' +@pytest.mark.get_page('https://gotmyowndoma.in') +def test_haketilodb_allowing(driver, execute_in_page): + """ + indexeddb.js facilitates operating on Haketilo's internal database. + Verify changing the "blocking" configuration for a URL works properly. + """ + execute_in_page(indexeddb_js()) + mock_broadcast(execute_in_page) + + # Start with no database. + clear_indexeddb(execute_in_page) + + assert get_db_contents(execute_in_page)['blocking'] == [] + + def run_with_sample_url(expr): + return execute_in_page(f'returnval({expr});', 'https://example.com/**') + + assert None == run_with_sample_url('get_allowing(arguments[0])') + + run_with_sample_url('set_disallowed(arguments[0])') + assert False == run_with_sample_url('get_allowing(arguments[0])') + + run_with_sample_url('set_allowed(arguments[0])') + assert True == run_with_sample_url('get_allowing(arguments[0])') + + run_with_sample_url('set_default_allowing(arguments[0])') + assert None == run_with_sample_url('get_allowing(arguments[0])') + +@pytest.mark.get_page('https://gotmyowndoma.in') +def test_haketilodb_repos(driver, execute_in_page): + """ + indexeddb.js facilitates operating on Haketilo's internal database. + Verify operations on repositories list work properly. + """ + execute_in_page(indexeddb_js()) + mock_broadcast(execute_in_page) + + # Start with no database. + clear_indexeddb(execute_in_page) + + assert get_db_contents(execute_in_page)['repos'] == [] + + sample_urls = ['https://hdrlla.example.com/', 'https://hdrlla.example.org'] + + assert [] == execute_in_page('returnval(get_repos());') + + execute_in_page('returnval(set_repo(arguments[0]));', sample_urls[0]) + assert [sample_urls[0]] == execute_in_page('returnval(get_repos());') + + execute_in_page('returnval(set_repo(arguments[0]));', sample_urls[1]) + assert set(sample_urls) == set(execute_in_page('returnval(get_repos());')) + + execute_in_page('returnval(del_repo(arguments[0]));', sample_urls[0]) + assert [sample_urls[1]] == execute_in_page('returnval(get_repos());') + test_page_html = ''' - - -

resources

- -

mappings

- -

settings

- + + ''' @pytest.mark.ext_data({ @@ -342,13 +390,27 @@ def test_haketilodb_track(driver, execute_in_page, wait_elem_text): through extension's background script and allow for object store contents to be tracked in any execution context. """ - # Let's open the same extension's test page in a second window. Window 0 - # will be used to make changed to IndexedDB and window 1 to "track" those + # Let's open the same extension's test page in a second window. Window 1 + # will be used to make changes to IndexedDB and window 0 to "track" those # changes. driver.execute_script('window.open(window.location.href, "_blank");') windows = [*driver.window_handles] assert len(windows) == 2 + # Create elements that will have tracked data inserted under them. + driver.switch_to.window(windows[0]) + execute_in_page(''' + for (const store_name of trackable) { + const h2 = document.createElement("h2"); + h2.innerText = store_name; + document.body.append(h2); + + const ul = document.createElement("ul"); + ul.id = store_name; + document.body.append(ul); + } + ''') + # Mock initial_data. sample_resource = make_sample_resource() sample_mapping = make_sample_mapping() @@ -365,22 +427,19 @@ def test_haketilodb_track(driver, execute_in_page, wait_elem_text): }, 'files': sample_files_by_hash } - for window in reversed(windows): - driver.switch_to.window(window) - try : - driver.execute_script('console.log("uuuuuuu");') - execute_in_page('initial_data = arguments[0];', initial_data) - except: - from time import sleep - sleep(100000) - execute_in_page('returnval(set_setting("option15", "123"));') + driver.switch_to.window(windows[1]) + execute_in_page('initial_data = arguments[0];', initial_data) + execute_in_page('returnval(set_setting("option15", "123"));') + execute_in_page('returnval(set_repo("https://hydril.la"));') + execute_in_page('returnval(set_disallowed("file:///*"));') # See if track.*() functions properly return the already-existing items. + driver.switch_to.window(windows[0]) execute_in_page( ''' function update_item(store_name, change) { - console.log('update', ...arguments); + console.log('# update', ...arguments); const elem_id = `${store_name}_${change.key}`; let elem = document.getElementById(elem_id); elem = elem || document.createElement("li"); @@ -395,10 +454,11 @@ def test_haketilodb_track(driver, execute_in_page, wait_elem_text): async function start_reporting() { - for (const store_name of ["resources", "mappings", "settings"]) { + const props = new Map(stores.map(([sn, opt]) => [sn, opt.keyPath])); + for (const store_name of trackable) { [tracking, items] = await track[store_name](ch => update_item(store_name, ch)); - const prop = store_name === "settings" ? "name" : "identifier"; + const prop = props.get(store_name); for (const item of items) update_item(store_name, {key: item[prop], new_val: item}); } @@ -407,18 +467,20 @@ def test_haketilodb_track(driver, execute_in_page, wait_elem_text): returnval(start_reporting()); ''') - item_counts = driver.execute_script( - ''' + item_counts = execute_in_page( + '''{ const childcount = id => document.getElementById(id).childElementCount; - return ["resources", "mappings", "settings"].map(childcount); - ''') - assert item_counts == [1, 1, 1] - resource_json = driver.find_element_by_id('resources_helloapple').text - mapping_json = driver.find_element_by_id('mappings_helloapple').text - setting_json = driver.find_element_by_id('settings_option15').text - assert json.loads(resource_json) == sample_resource - assert json.loads(mapping_json) == sample_mapping - assert json.loads(setting_json) == {'name': 'option15', 'value': '123'} + returnval(trackable.map(childcount)); + }''') + assert item_counts == [1 for _ in item_counts] + for elem_id, json_value in [ + ('resources_helloapple', sample_resource), + ('mappings_helloapple', sample_mapping), + ('settings_option15', {'name': 'option15', 'value': '123'}), + ('repos_https://hydril.la', {'url': 'https://hydril.la'}), + ('blocking_file:///*', {'pattern': 'file:///*', 'allow': False}) + ]: + assert json.loads(driver.find_element_by_id(elem_id).text) == json_value # See if item additions get tracked properly. driver.switch_to.window(windows[1]) @@ -441,22 +503,26 @@ def test_haketilodb_track(driver, execute_in_page, wait_elem_text): } execute_in_page('returnval(save_items(arguments[0]));', sample_data) execute_in_page('returnval(set_setting("option22", "abc"));') + execute_in_page('returnval(set_repo("https://hydril2.la"));') + execute_in_page('returnval(set_allowed("ftp://a.bc/"));') driver.switch_to.window(windows[0]) driver.implicitly_wait(10) - resource_json = driver.find_element_by_id('resources_helloapple-copy').text - mapping_json = driver.find_element_by_id('mappings_helloapple-copy').text - setting_json = driver.find_element_by_id('settings_option22').text + for elem_id, json_value in [ + ('resources_helloapple-copy', sample_resource2), + ('mappings_helloapple-copy', sample_mapping2), + ('settings_option22', {'name': 'option22', 'value': 'abc'}), + ('repos_https://hydril2.la', {'url': 'https://hydril2.la'}), + ('blocking_ftp://a.bc/', {'pattern': 'ftp://a.bc/', 'allow': True}) + ]: + assert json.loads(driver.find_element_by_id(elem_id).text) == json_value driver.implicitly_wait(0) - assert json.loads(resource_json) == sample_resource2 - assert json.loads(mapping_json) == sample_mapping2 - assert json.loads(setting_json) == {'name': 'option22', 'value': 'abc'} - # See if item deletions get tracked properly. + # See if item deletions/modifications get tracked properly. driver.switch_to.window(windows[1]) execute_in_page( '''{ - async function remove_items() + async function change_remove_items() { const store_names = ["resources", "mappings"]; const ctx = await start_items_transaction(store_names, {}); @@ -464,20 +530,27 @@ def test_haketilodb_track(driver, execute_in_page, wait_elem_text): await remove_mapping("helloapple-copy", ctx); await finalize_transaction(ctx); await set_setting("option22", null); + await del_repo("https://hydril.la"); + await set_default_allowing("file:///*"); + await set_disallowed("ftp://a.bc/"); } - returnval(remove_items()); + returnval(change_remove_items()); }''') - removed_ids = ['mappings_helloapple-copy', 'resources_helloapple'] - def condition_items_absent(driver): + removed_ids = ['mappings_helloapple-copy', 'resources_helloapple', + 'repos_https://hydril.la', 'blocking_file:///*'] + def condition_items_absent_and_changed(driver): for id in removed_ids: try: driver.find_element_by_id(id) return False except WebDriverException: pass + option_text = driver.find_element_by_id('settings_option22').text - return json.loads(option_text)['value'] == None + blocking_text = driver.find_element_by_id('blocking_ftp://a.bc/').text + return (json.loads(option_text)['value'] == None and + json.loads(blocking_text)['allow'] == False) driver.switch_to.window(windows[0]) - WebDriverWait(driver, 10).until(condition_items_absent) + WebDriverWait(driver, 10).until(condition_items_absent_and_changed) -- cgit v1.2.3