summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2022-01-03 12:11:23 +0100
committerWojtek Kosior <koszko@koszko.org>2022-01-03 12:11:23 +0100
commit0feb9db2b57725d47a7b3cc1e84ba5b9a7553b12 (patch)
tree94c87b244d6e8a4312c271824891dfa272b684ff
parent702eefd252a112375c2da6a9ae4b39915fc2dbf4 (diff)
downloadbrowser-extension-0feb9db2b57725d47a7b3cc1e84ba5b9a7553b12.tar.gz
browser-extension-0feb9db2b57725d47a7b3cc1e84ba5b9a7553b12.zip
add "blocking" and "repos" object stores
-rw-r--r--common/indexeddb.js81
-rw-r--r--test/unit/conftest.py3
-rw-r--r--test/unit/test_indexeddb.py169
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 = '''
<!DOCTYPE html>
<script src="/testpage.js"></script>
-<script>console.log("inline!")</script>
-<script nonce="123456789">console.log("inline nonce!")</script>
-<h2>resources</h2>
-<ul id="resources"></ul>
-<h2>mappings</h2>
-<ul id="mappings"></ul>
-<h2>settings</h2>
-<ul id="settings"></ul>
+<body>
+</body>
'''
@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)