aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWojtek Kosior <koszko@koszko.org>2022-03-15 10:12:06 +0100
committerWojtek Kosior <koszko@koszko.org>2022-03-24 20:43:40 +0100
commitbbc9fae4291d0c2cb3976d158ecd20e0bd2a8ea0 (patch)
tree9d72c3184f85b61d3d71bc2d65cddb8058d9222a
parent65351e8c69659455096d1e37c4b78d1298fb7021 (diff)
downloadbrowser-extension-bbc9fae4291d0c2cb3976d158ecd20e0bd2a8ea0.tar.gz
browser-extension-bbc9fae4291d0c2cb3976d158ecd20e0bd2a8ea0.zip
serialize and deserialize entire Response object when relaying fetch() calls to other contexts using sendMessage
-rw-r--r--background/CORS_bypass_server.js55
-rw-r--r--common/misc.js27
-rw-r--r--content/repo_query_cacher.js11
-rw-r--r--html/install.js57
-rw-r--r--html/repo_query.js30
-rw-r--r--html/repo_query_cacher_client.js85
-rw-r--r--test/haketilo_test/unit/test_CORS_bypass_server.py65
-rw-r--r--test/haketilo_test/unit/test_install.py59
-rw-r--r--test/haketilo_test/unit/test_popup.py22
-rw-r--r--test/haketilo_test/unit/test_repo_query.py64
-rw-r--r--test/haketilo_test/unit/test_repo_query_cacher.py19
-rw-r--r--test/haketilo_test/unit/utils.py58
12 files changed, 334 insertions, 218 deletions
diff --git a/background/CORS_bypass_server.js b/background/CORS_bypass_server.js
index 9805d41..d15db54 100644
--- a/background/CORS_bypass_server.js
+++ b/background/CORS_bypass_server.js
@@ -2,7 +2,7 @@
* This file is part of Haketilo.
*
* Function: Allow other parts of the extension to bypass CORS by routing their
- * request through this background script using one-off messages.
+ * requests through this background script using one-off messages.
*
* Copyright (C) 2022 Wojtek Kosior <koszko@koszko.org>
*
@@ -43,35 +43,40 @@
*/
#FROM common/browser.js IMPORT browser
+#FROM common/misc.js IMPORT uint8_to_hex, error_data_jsonifiable
-async function get_prop(object, prop, result_object, call_prop=false) {
- try {
- result_object[prop] = call_prop ? (await object[prop]()) : object[prop];
- } catch(e) {
- result_object[`error_${prop}`] = "" + e;
- }
+/*
+ * In this file we implement a fetch()-from-background-script service. Code in
+ * other parts of the extension shall call sendMessage() with arguments to
+ * fetch() and code here will call fetch() with those arguments and send back
+ * the response.
+ *
+ * We have to convert the Response from fetch() into a JSON-ifiable value and
+ * then convert it back (using Response() constructor) due (among others) the
+ * limitations of Chromium's messaging API:
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Chrome_incompatibilities#data_cloning_algorithm
+ *
+ * We also catch possible errors from fetch() (e.g. in case of an invalid URL)
+ * and send their data in JSON-ifiable form so that the error object can be
+ * later re-created.
+ */
+
+/* Make it possible to serialize Response object. */
+async function response_data_jsonifiable(response) {
+ return {
+ status: response.status,
+ statusText: response.statusText,
+ headers: [...response.headers.entries()],
+ body: uint8_to_hex(new Uint8Array(await response.arrayBuffer()))
+ };
}
-async function perform_download(fetch_data, respond_cb) {
+async function perform_download(fetch_data) {
try {
- const response = await fetch(fetch_data.url);
- const result = {};
-
- for (const prop of (fetch_data.to_get || []))
- get_prop(response, prop, result);
-
- const to_call = (fetch_data.to_call || []);
- const promises = [];
- for (let i = 0; i < to_call.length; i++) {
- const response_to_use = i === to_call.length - 1 ?
- response : response.clone();
- promises.push(get_prop(response_to_use, to_call[i], result, true));
- }
-
- await Promise.all(promises);
- return result;
+ const response = await fetch(fetch_data.url, fetch_data.init);
+ return response_data_jsonifiable(response);
} catch(e) {
- return {error: "" + e};
+ return {error: error_data_jsonifiable(e)};
}
}
diff --git a/common/misc.js b/common/misc.js
index e609fe0..8096f27 100644
--- a/common/misc.js
+++ b/common/misc.js
@@ -42,9 +42,13 @@
* proprietary program, I am not going to enforce this in court.
*/
-/* uint8_to_hex is a separate function used in cryptographic functions. */
+/*
+ * uint8_to_hex is a separate function used in cryptographic functions and when
+ * dealing with binary data.
+ */
const uint8_to_hex =
array => [...array].map(b => ("0" + b.toString(16)).slice(-2)).join("");
+#EXPORT uint8_to_hex
/*
* Asynchronously compute hex string representation of a sha256 digest of a
@@ -61,8 +65,7 @@ async function sha256_async(string) {
* Generate a unique value that can be computed synchronously and is impossible
* to guess for a malicious website.
*/
-function gen_nonce(length=16)
-{
+function gen_nonce(length=16) {
const random_data = new Uint8Array(length);
crypto.getRandomValues(random_data);
return uint8_to_hex(random_data);
@@ -71,16 +74,10 @@ function gen_nonce(length=16)
/* Check if some HTTP header might define CSP rules. */
const csp_header_regex =
- /^\s*(content-security-policy|x-webkit-csp|x-content-security-policy)/i;
+ /^\s*(content-security-policy|x-webkit-csp|x-content-security-policy)\s*$/i;
#EXPORT csp_header_regex
/*
- * Print item together with type, e.g.
- * nice_name("s", "hello") → "hello (script)"
- */
-#EXPORT (prefix, name) => `${name} (${TYPE_NAME[prefix]})` AS nice_name
-
-/*
* Check if url corresponds to a browser's special page (or a directory index in
* case of `file://' protocol).
*/
@@ -90,3 +87,13 @@ const priv_reg = /^moz-extension:\/\/|^about:|^view-source:|^file:\/\/[^?#]*\/([
const priv_reg = /^chrome(-extension)?:\/\/|^about:|^view-source:|^file:\/\/[^?#]*\/([?#]|$)/;
#ENDIF
#EXPORT url => priv_reg.test(url) AS is_privileged_url
+
+/* Make it possible to serialize en Error object. */
+function error_data_jsonifiable(error) {
+ const jsonifiable = {};
+ for (const property of ["name", "message", "fileName", "lineNumber"])
+ jsonifiable[property] = error[property];
+
+ return jsonifiable;
+}
+#EXPORT error_data_jsonifiable
diff --git a/content/repo_query_cacher.js b/content/repo_query_cacher.js
index 3f62be7..be97c39 100644
--- a/content/repo_query_cacher.js
+++ b/content/repo_query_cacher.js
@@ -43,10 +43,12 @@
*/
#FROM common/browser.js IMPORT browser
+#FROM common/misc.js IMPORT error_data_jsonifiable
/*
* Map URLs to objects containing parsed responses, error info or promises
- * resolving to those.
+ * resolving to those. The use of promises helps us prevent multiple requests
+ * for the same resource from starting concurrently.
*/
const cache = new Map();
@@ -58,12 +60,11 @@ async function perform_download(url) {
cache.set(url, new Promise(cb => resolve_cb = cb));
try {
- const opts = {url, to_get: ["ok", "status"], to_call: ["json"]};
- var result = await browser.runtime.sendMessage(["CORS_bypass", opts]);
+ var result = await browser.runtime.sendMessage(["CORS_bypass", {url}]);
if (result === undefined)
- result = {error: "Couldn't communicate with background script."};
+ throw new Error("Couldn't communicate with background script.");
} catch(e) {
- var result = {error: e + ""};
+ return {error: error_data_jsonifiable(e)};
}
cache.set(url, result);
diff --git a/html/install.js b/html/install.js
index fddee5d..a066b9b 100644
--- a/html/install.js
+++ b/html/install.js
@@ -51,6 +51,8 @@
#FROM common/misc.js IMPORT sha256_async AS compute_sha256
#FROM common/jsonschema.js IMPORT haketilo_validator, haketilo_schemas
+#FROM html/repo_query_cacher_client.js IMPORT indirect_fetch
+
const coll = new Intl.Collator();
/*
@@ -104,6 +106,9 @@ async function init_work() {
};
work.err = function (error, user_message) {
+ if (!this.is_ok)
+ return;
+
if (error)
console.error("Haketilo:", error);
work.is_ok = false;
@@ -171,35 +176,40 @@ function InstallView(tab_id, on_view_show, on_view_hide) {
const url = ver ?
`${this.repo_url}${item_type}/${id}/${ver.join(".")}` :
`${this.repo_url}${item_type}/${id}.json`;
- const response =
- await browser.tabs.sendMessage(tab_id, ["repo_query", url]);
- if (!work.is_ok)
- return;
- if ("error" in response) {
- return work.err(response.error,
- "Failure to communicate with repository :(");
+
+ try {
+ var response = await indirect_fetch(tab_id, url);
+ } catch(e) {
+ return work.err(e, "Failure to communicate with repository :(");
}
+ if (!work.is_ok)
+ return;
+
if (!response.ok) {
return work.err(null,
`Repository sent HTTP code ${response.status} :(`);
}
- if ("error_json" in response) {
- return work.err(response.error_json,
- "Repository's response is not valid JSON :(");
+ try {
+ var json = await response.json();
+ } catch(e) {
+ return work.err(e, "Repository's response is not valid JSON :(");
}
+ if (!work.is_ok)
+ return;
+
const captype = item_type[0].toUpperCase() + item_type.substring(1);
const $id =
`https://hydrilla.koszko.org/schemas/api_${item_type}_description-1.0.1.schema.json`;
const schema = haketilo_schemas[$id];
- const result = haketilo_validator.validate(response.json, schema);
+ const result = haketilo_validator.validate(json, schema);
if (result.errors.length > 0) {
const reg = new RegExp(schema.allOf[2].properties.$schema.pattern);
- if (response.json.$schema && !reg.test(response.json.$schema)) {
+ if (json.$schema && !reg.test(json.$schema)) {
const msg = `${captype} ${item_id_string(id, ver)} was served using unsupported Hydrilla API version. You might need to update Haketilo.`;
return work.err(result.errors, msg);
}
@@ -208,14 +218,14 @@ function InstallView(tab_id, on_view_show, on_view_hide) {
return work.err(result.errors, msg);
}
- const scripts = item_type === "resource" && response.json.scripts;
- const files = response.json.source_copyright.concat(scripts || []);
+ const scripts = item_type === "resource" && json.scripts;
+ const files = json.source_copyright.concat(scripts || []);
if (item_type === "mapping") {
- for (const res_ref of Object.values(response.json.payloads || {}))
+ for (const res_ref of Object.values(json.payloads || {}))
process_item(work, "resource", res_ref.identifier);
} else {
- for (const res_ref of (response.json.dependencies || []))
+ for (const res_ref of (json.dependencies || []))
process_item(work, "resource", res_ref.identifier);
}
@@ -234,8 +244,8 @@ function InstallView(tab_id, on_view_show, on_view_hide) {
const msg = "Error accessing Haketilo's internal database :(";
return work.err(e, msg);
}
- if (!db_def || db_def.version < response.json.version)
- work.result.push({def: response.json, db_def});
+ if (!db_def || db_def.version < json.version)
+ work.result.push({def: json, db_def});
if (--work.waiting === 0)
work.resolve_cb(work.result);
@@ -320,14 +330,9 @@ function InstallView(tab_id, on_view_show, on_view_hide) {
return work.err(null, msg);
}
- try {
- var text = await response.text();
- if (!work.is_ok)
- return;
- } catch(e) {
- const msg = "Repository's response is not valid text :(";
- return work.err(e, msg);
- }
+ const text = await response.text();
+ if (!work.is_ok)
+ return;
const digest = await compute_sha256(text);
if (!work.is_ok)
diff --git a/html/repo_query.js b/html/repo_query.js
index 601a0aa..97f60ac 100644
--- a/html/repo_query.js
+++ b/html/repo_query.js
@@ -49,6 +49,8 @@
#FROM html/install.js IMPORT InstallView
#FROM common/jsonschema.js IMPORT haketilo_validator, haketilo_schemas
+#FROM html/repo_query_cacher_client.js IMPORT indirect_fetch
+
const coll = new Intl.Collator();
function ResultEntry(repo_entry, mapping_ref) {
@@ -76,36 +78,42 @@ function RepoEntry(query_view, repo_url) {
this.repo_url_label.innerText = repo_url;
- const query_results = async () => {
- const msg = [
- "repo_query",
- `${repo_url}query?url=${encodeURIComponent(query_view.url)}`
- ];
- const response = await browser.tabs.sendMessage(query_view.tab_id, msg);
+ const encoded_queried_url = encodeURIComponent(query_view.url);
+ const query_url = `${repo_url}query?url=${encoded_queried_url}`;
- if ("error" in response)
+ const query_results = async () => {
+ try {
+ var response = await indirect_fetch(query_view.tab_id, query_url);
+ } catch(e) {
+ console.error("Haketilo:", e);
throw "Failure to communicate with repository :(";
+ }
if (!response.ok)
throw `Repository sent HTTP code ${response.status} :(`;
- if ("error_json" in response)
+
+ try {
+ var json = await response.json();
+ } catch(e) {
+ console.error("Haketilo:", e);
throw "Repository's response is not valid JSON :(";
+ }
const $id =
`https://hydrilla.koszko.org/schemas/api_query_result-1.0.1.schema.json`;
const schema = haketilo_schemas[$id];
- const result = haketilo_validator.validate(response.json, schema);
+ const result = haketilo_validator.validate(json, schema);
if (result.errors.length > 0) {
console.error("Haketilo:", result.errors);
const reg = new RegExp(schema.properties.$schema.pattern);
- if (response.json.$schema && !reg.test(response.json.$schema))
+ if (json.$schema && !reg.test(json.$schema))
throw "Results were served using unsupported Hydrilla API version. You might need to update Haketilo.";
throw "Results were served using a nonconforming response format.";
}
- return response.json.mappings;
+ return json.mappings;
}
const populate_results = async () => {
diff --git a/html/repo_query_cacher_client.js b/html/repo_query_cacher_client.js
new file mode 100644
index 0000000..b7f49e9
--- /dev/null
+++ b/html/repo_query_cacher_client.js
@@ -0,0 +1,85 @@
+/**
+ * This file is part of Haketilo.
+ *
+ * Function: Making requests to remote repositories through a response cache in
+ * operating in the content script of a browser tab.
+ *
+ * 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 GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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
+ * GNU General Public License for more details.
+ *
+ * As additional permission under GNU GPL version 3 section 7, you
+ * may distribute forms of that code without the copy of the GNU
+ * GPL normally required by section 4, provided you include this
+ * license notice and, in case of non-source distribution, a URL
+ * through which recipients can access the Corresponding Source.
+ * If you modify file(s) with this exception, you may extend this
+ * exception to your version of the file(s), but you are not
+ * obligated to do so. If you do not wish to do so, delete this
+ * exception statement from your version.
+ *
+ * As a special exception to the GPL, any HTML file which merely
+ * makes function calls to this code, and for that purpose
+ * includes it by reference shall be deemed a separate work for
+ * copyright law purposes. If you modify this code, you may extend
+ * this exception to your version of the code, but you are not
+ * obligated to do so. If you do not wish to do so, delete this
+ * exception statement from your version.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
+ * license. Although I request that you do not make use of this code in a
+ * proprietary program, I am not going to enforce this in court.
+ */
+
+#FROM common/browser.js IMPORT browser
+
+/*
+ * This is a complementary function to error_data_jsonifiable() in
+ * common/misc.js.
+ */
+function error_from_jsonifiable_data(jsonifiable) {
+ const arg_props = ["message", "fileName", "lineNumber"];
+ return new window[jsonifiable.name](...arg_props.map(p => jsonifiable[p]));
+}
+
+/*
+ * Make it possible to recover a Response object. This is a complementary
+ * function to response_data_jsonifiable() in background/CORS_bypass_server.js.
+ */
+function response_from_jsonifiable_data(jsonifiable) {
+ const body = jsonifiable.body, body_len = body.length / 2;
+ const body_buf = new Uint8Array(body_len);
+
+ for (let i = 0; i < body_len; i++)
+ body_buf[i] = parseInt(`0x${body.substring(i * 2, i * 2 + 2)}`);
+
+ const init = {
+ status: jsonifiable.status,
+ statusText: jsonifiable.statusText,
+ headers: new Headers(jsonifiable.headers)
+ };
+
+ return new Response(body_buf, init);
+}
+
+async function indirect_fetch(tab_id, url) {
+ const msg = ["repo_query", url];
+ const jsonifiable = await browser.tabs.sendMessage(tab_id, msg);
+
+ if ("error" in jsonifiable)
+ throw error_from_jsonifiable_data(jsonifiable);
+
+ return response_from_jsonifiable_data(jsonifiable);
+}
+#EXPORT indirect_fetch
diff --git a/test/haketilo_test/unit/test_CORS_bypass_server.py b/test/haketilo_test/unit/test_CORS_bypass_server.py
index 45e4ebb..45f06a9 100644
--- a/test/haketilo_test/unit/test_CORS_bypass_server.py
+++ b/test/haketilo_test/unit/test_CORS_bypass_server.py
@@ -24,29 +24,28 @@ from selenium.webdriver.support.ui import WebDriverWait
from ..script_loader import load_script
from ..world_wide_library import some_data
-urls = {
- 'resource': 'https://anotherdoma.in/resource/blocked/by/CORS.json',
- 'nonexistent': 'https://nxdoma.in/resource.json',
- 'invalid': 'w3csucks://invalid.url/'
+datas = {
+ 'resource': 'https://anotherdoma.in/resource/blocked/by/CORS.json',
+ 'nonexistent': 'https://nxdoma.in/resource.json',
+ 'invalid': 'w3csucks://invalid.url/',
+ 'redirected_ok': 'https://site.with.scripts.block.ed',
+ 'redirected_err': 'https://site.with.scripts.block.ed'
}
+for name, url in [*datas.items()]:
+ datas[name] = {'url': url}
+
+datas['redirected_ok']['init'] = {'redirect': 'follow'}
+datas['redirected_err']['init'] = {'redirect': 'error'}
+
content_script = '''\
-const urls = %s;
-
-function fetch_data(url) {
- return {
- url,
- to_get: ["ok", "status"],
- to_call: ["text", "json"]
- };
-}
+const datas = %s;
async function fetch_resources() {
const results = {};
const promises = [];
- for (const [name, url] of Object.entries(urls)) {
- const sending = browser.runtime.sendMessage(["CORS_bypass",
- fetch_data(url)]);
+ for (const [name, data] of Object.entries(datas)) {
+ const sending = browser.runtime.sendMessage(["CORS_bypass", data]);
promises.push(sending.then(response => results[name] = response));
}
@@ -58,7 +57,7 @@ async function fetch_resources() {
fetch_resources();
'''
-content_script = content_script % json.dumps(urls);
+content_script = content_script % json.dumps(datas);
@pytest.mark.ext_data({
'content_script': content_script,
@@ -77,33 +76,41 @@ def test_CORS_bypass_server(driver, execute_in_page):
'''
const result = {};
let promises = [];
- for (const [name, url] of Object.entries(arguments[0])) {
+ for (const [name, data] of Object.entries(arguments[0])) {
const [ok_cb, err_cb] =
["ok", "err"].map(status => () => result[name] = status);
- promises.push(fetch(url).then(ok_cb, err_cb));
+ promises.push(fetch(data.url).then(ok_cb, err_cb));
}
// Make the promises non-failing.
promises = promises.map(p => new Promise(cb => p.then(cb, cb)));
returnval(Promise.all(promises).then(() => result));
''',
- {**urls, 'sameorigin': './nonexistent_resource'})
+ {**datas, 'sameorigin': './nonexistent_resource'})
- assert results == dict([*[(k, 'err') for k in urls.keys()],
+ assert results == dict([*[(k, 'err') for k in datas.keys()],
('sameorigin', 'ok')])
done = lambda d: d.execute_script('return window.haketilo_fetch_results;')
results = WebDriverWait(driver, 10).until(done)
assert set(results['invalid'].keys()) == {'error'}
+ assert results['invalid']['error']['fileName'].endswith('background.js')
+ assert type(results['invalid']['error']['lineNumber']) is int
+ assert type(results['invalid']['error']['message']) is str
+ assert results['invalid']['error']['name'] == 'TypeError'
- assert set(results['nonexistent'].keys()) == \
- {'ok', 'status', 'text', 'error_json'}
- assert results['nonexistent']['ok'] == False
assert results['nonexistent']['status'] == 404
- assert results['nonexistent']['text'] == 'Handler for this URL not found.'
+ assert results['nonexistent']['statusText'] == 'Not Found'
+ assert any([name.lower() == 'content-length'
+ for name, value in results['nonexistent']['headers']])
+ assert bytes.fromhex(results['nonexistent']['body']) == \
+ b'Handler for this URL not found.'
- assert set(results['resource'].keys()) == {'ok', 'status', 'text', 'json'}
- assert results['resource']['ok'] == True
assert results['resource']['status'] == 200
- assert results['resource']['text'] == some_data
- assert results['resource']['json'] == json.loads(some_data)
+ assert results['resource']['statusText'] == 'OK'
+ assert any([name.lower() == 'content-length'
+ for name, value in results['resource']['headers']])
+ assert bytes.fromhex(results['resource']['body']) == b'{"some": "data"}'
+
+ assert results['redirected_ok']['status'] == 200
+ assert results['redirected_err']['error']['name'] == 'TypeError'
diff --git a/test/haketilo_test/unit/test_install.py b/test/haketilo_test/unit/test_install.py
index 1e2063c..29910cf 100644
--- a/test/haketilo_test/unit/test_install.py
+++ b/test/haketilo_test/unit/test_install.py
@@ -26,7 +26,7 @@ from ..script_loader import load_script
from .utils import *
def setup_view(driver, execute_in_page):
- mock_cacher(execute_in_page)
+ execute_in_page(mock_cacher_code)
execute_in_page(load_script('html/install.js'))
container_ids, containers_objects = execute_in_page(
@@ -203,7 +203,6 @@ def test_install_normal_usage(driver, execute_in_page, complex_variant):
'indexeddb_error_file_uses',
'failure_to_communicate_fetch',
'HTTP_code_file',
- 'not_valid_text',
'sha256_mismatch',
'indexeddb_error_write'
])
@@ -243,7 +242,7 @@ def test_install_dialogs(driver, execute_in_page, message):
if message == 'fetching_data':
execute_in_page(
'''
- browser.tabs.sendMessage = () => new Promise(cb => {});
+ window.mock_cacher_fetch = () => new Promise(cb => {});
install_view.show(...arguments);
''',
'https://hydril.la/', 'mapping', 'mapping-a')
@@ -253,7 +252,8 @@ def test_install_dialogs(driver, execute_in_page, message):
elif message == 'failure_to_communicate_sendmessage':
execute_in_page(
'''
- browser.tabs.sendMessage = () => Promise.resolve({error: "sth"});
+ window.mock_cacher_fetch =
+ () => {throw new Error("Something happened :o")};
install_view.show(...arguments);
''',
'https://hydril.la/', 'mapping', 'mapping-a')
@@ -262,8 +262,8 @@ def test_install_dialogs(driver, execute_in_page, message):
elif message == 'HTTP_code_item':
execute_in_page(
'''
- const response = {ok: false, status: 404};
- browser.tabs.sendMessage = () => Promise.resolve(response);
+ const response = new Response("", {status: 404});
+ window.mock_cacher_fetch = () => Promise.resolve(response);
install_view.show(...arguments);
''',
'https://hydril.la/', 'mapping', 'mapping-a')
@@ -272,8 +272,8 @@ def test_install_dialogs(driver, execute_in_page, message):
elif message == 'invalid_JSON':
execute_in_page(
'''
- const response = {ok: true, status: 200, error_json: "sth"};
- browser.tabs.sendMessage = () => Promise.resolve(response);
+ const response = new Response("sth", {status: 200});
+ window.mock_cacher_fetch = () => Promise.resolve(response);
install_view.show(...arguments);
''',
'https://hydril.la/', 'mapping', 'mapping-a')
@@ -282,12 +282,11 @@ def test_install_dialogs(driver, execute_in_page, message):
elif message == 'newer_API_version':
execute_in_page(
'''
- const old_sendMessage = browser.tabs.sendMessage;
- browser.tabs.sendMessage = async function(...args) {
- const response = await old_sendMessage(...args);
- response.json.$schema = "https://hydrilla.koszko.org/schemas/api_mapping_description-255.1.schema.json";
- return response;
- }
+ 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])
@@ -297,12 +296,18 @@ def test_install_dialogs(driver, execute_in_page, message):
elif message == 'invalid_response_format':
execute_in_page(
'''
- const old_sendMessage = browser.tabs.sendMessage;
- browser.tabs.sendMessage = async function(...args) {
- const response = await old_sendMessage(...args);
- /* identifier is not a string as it should be. */
- response.json.identifier = 1234567;
- return response;
+ 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);
''',
@@ -352,7 +357,7 @@ def test_install_dialogs(driver, execute_in_page, message):
elif message == 'failure_to_communicate_fetch':
execute_in_page(
'''
- fetch = () => {throw "some error";};
+ fetch = () => {throw new Error("some error");};
returnval(install_view.show(...arguments));
''',
'https://hydril.la/', 'mapping', 'mapping-b')
@@ -372,18 +377,6 @@ def test_install_dialogs(driver, execute_in_page, message):
execute_in_page('returnval(install_view.install_but);').click()
assert_dlg(['conf_buts'], 'Repository sent HTTP code 400 :(')
- elif message == 'not_valid_text':
- execute_in_page(
- '''
- const err = () => {throw "some error";};
- fetch = () => Promise.resolve({ok: true, status: 200, text: err});
- 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's response is not valid text :(")
elif message == 'sha256_mismatch':
execute_in_page(
'''
diff --git a/test/haketilo_test/unit/test_popup.py b/test/haketilo_test/unit/test_popup.py
index e62feb7..3ef7906 100644
--- a/test/haketilo_test/unit/test_popup.py
+++ b/test/haketilo_test/unit/test_popup.py
@@ -81,28 +81,18 @@ mocked_page_infos = {
tab_mock_js = '''
;
const mocked_page_info = (%s)[/#mock_page_info-(.*)$/.exec(document.URL)[1]];
+const old_sendMessage = browser.tabs.sendMessage;
browser.tabs.sendMessage = async function(tab_id, msg) {
const this_tab_id = (await browser.tabs.getCurrent()).id;
if (tab_id !== this_tab_id)
throw `not current tab id (${tab_id} instead of ${this_tab_id})`;
- if (msg[0] === "page_info") {
+ if (msg[0] === "page_info")
return mocked_page_info;
- } else if (msg[0] === "repo_query") {
- const response = await fetch(msg[1]);
- if (!response)
- return {error: "Something happened :o"};
-
- const result = {ok: response.ok, status: response.status};
- try {
- result.json = await response.json();
- } catch(e) {
- result.error_json = "" + e;
- }
- return result;
- } else {
+ else if (msg[0] === "repo_query")
+ return old_sendMessage(tab_id, msg);
+ else
throw `bad sendMessage message type: '${msg[0]}'`;
- }
}
const old_tabs_query = browser.tabs.query;
@@ -113,6 +103,8 @@ browser.tabs.query = async function(query) {
}
''' % json.dumps(mocked_page_infos)
+tab_mock_js = mock_cacher_code + tab_mock_js
+
popup_ext_data = {
'background_script': broker_js,
'extra_html': ExtraHTML(
diff --git a/test/haketilo_test/unit/test_repo_query.py b/test/haketilo_test/unit/test_repo_query.py
index f6cae93..c785406 100644
--- a/test/haketilo_test/unit/test_repo_query.py
+++ b/test/haketilo_test/unit/test_repo_query.py
@@ -29,7 +29,7 @@ repo_urls = [f'https://hydril.la/{s}' for s in ('', '1/', '2/', '3/', '4/')]
queried_url = 'https://example_a.com/something'
def setup_view(execute_in_page, repo_urls, tab={'id': 0}):
- mock_cacher(execute_in_page)
+ execute_in_page(mock_cacher_code)
execute_in_page(load_script('html/repo_query.js'))
execute_in_page(
@@ -185,8 +185,10 @@ def test_repo_query_messages(driver, execute_in_page, message):
elif message == 'failure_to_communicate':
setup_view(execute_in_page, repo_urls)
execute_in_page(
- 'browser.tabs.sendMessage = () => Promise.resolve({error: "sth"});'
- )
+ '''
+ window.mock_cacher_fetch =
+ () => {throw new Error("Something happened :o")};
+ ''')
show_and_wait_for_repo_entry()
elem = execute_in_page('returnval(view.repo_entries[0].info_div);')
@@ -196,8 +198,8 @@ def test_repo_query_messages(driver, execute_in_page, message):
setup_view(execute_in_page, repo_urls)
execute_in_page(
'''
- const response = {ok: false, status: 405};
- browser.tabs.sendMessage = () => Promise.resolve(response);
+ const response = new Response("", {status: 405});
+ window.mock_cacher_fetch = () => Promise.resolve(response);
''')
show_and_wait_for_repo_entry()
@@ -208,8 +210,8 @@ def test_repo_query_messages(driver, execute_in_page, message):
setup_view(execute_in_page, repo_urls)
execute_in_page(
'''
- const response = {ok: true, status: 200, error_json: "sth"};
- browser.tabs.sendMessage = () => Promise.resolve(response);
+ const response = new Response("sth", {status: 200});
+ window.mock_cacher_fetch = () => Promise.resolve(response);
''')
show_and_wait_for_repo_entry()
@@ -220,12 +222,11 @@ def test_repo_query_messages(driver, execute_in_page, message):
setup_view(execute_in_page, repo_urls)
execute_in_page(
'''
- const response = {
- ok: true,
- status: 200,
- json: {$schema: "https://hydrilla.koszko.org/schemas/api_query_result-255.2.1.schema.json"}
- };
- browser.tabs.sendMessage = () => Promise.resolve(response);
+ const newer_schema_url =
+ "https://hydrilla.koszko.org/schemas/api_query_result-255.2.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);
''')
show_and_wait_for_repo_entry()
@@ -236,13 +237,19 @@ def test_repo_query_messages(driver, execute_in_page, message):
setup_view(execute_in_page, repo_urls)
execute_in_page(
'''
- const response = {
- ok: true,
- status: 200,
- /* $schema is not a string as it should be. */
- json: {$schema: null}
- };
- browser.tabs.sendMessage = () => Promise.resolve(response);
+ window.mock_cacher_fetch = async function(...args) {
+ const response = await fetch(...args);
+ const json = await response.json();
+
+ /* $schema is no longer a string as it should be. */
+ json.$schema = null;
+
+ return new Response(JSON.stringify(json), {
+ status: response.status,
+ statusText: response.statusText,
+ headers: [...response.headers.entries()]
+ });
+ }
''')
show_and_wait_for_repo_entry()
@@ -252,7 +259,7 @@ def test_repo_query_messages(driver, execute_in_page, message):
elif message == 'querying_repo':
setup_view(execute_in_page, repo_urls)
execute_in_page(
- 'browser.tabs.sendMessage = () => new Promise(() => {});'
+ 'window.mock_cacher_fetch = () => new Promise(cb => {});'
)
show_and_wait_for_repo_entry()
@@ -262,15 +269,12 @@ def test_repo_query_messages(driver, execute_in_page, message):
setup_view(execute_in_page, repo_urls)
execute_in_page(
'''
- const response = {
- ok: true,
- status: 200,
- json: {
- $schema: "https://hydrilla.koszko.org/schemas/api_query_result-1.schema.json",
- mappings: []
- }
- };
- browser.tabs.sendMessage = () => Promise.resolve(response);
+ const schema_url =
+ "https://hydrilla.koszko.org/schemas/api_query_result-1.schema.json";
+ const mocked_json_data =
+ JSON.stringify({$schema: schema_url, mappings: []});
+ const response = new Response(mocked_json_data, {status: 200});
+ window.mock_cacher_fetch = () => Promise.resolve(response);
''')
show_and_wait_for_repo_entry()
diff --git a/test/haketilo_test/unit/test_repo_query_cacher.py b/test/haketilo_test/unit/test_repo_query_cacher.py
index 5fbc5cd..3f0a00d 100644
--- a/test/haketilo_test/unit/test_repo_query_cacher.py
+++ b/test/haketilo_test/unit/test_repo_query_cacher.py
@@ -85,34 +85,35 @@ def run_content_script_in_new_window(driver, url):
'background_script': lambda: bypass_js() + ';' + tab_id_responder
})
@pytest.mark.usefixtures('webextension')
-def test_repo_query_cacher_normal_use(driver, execute_in_page):
+def test_repo_query_cacher_normal_use(driver):
"""
Test if HTTP requests made through our cacher return correct results.
"""
tab_id = run_content_script_in_new_window(driver, 'https://gotmyowndoma.in')
result = fetch_through_cache(driver, tab_id, 'https://counterdoma.in/')
- assert set(result.keys()) == {'ok', 'status', 'json'}
- counter_initial = result['json']['counter']
+ assert set(result.keys()) == {'status', 'statusText', 'headers', 'body'}
+ counter_initial = json.loads(bytes.fromhex(result['body']))['counter']
assert type(counter_initial) is int
for i in range(2):
result = fetch_through_cache(driver, tab_id, 'https://counterdoma.in/')
- assert result['json']['counter'] == counter_initial
+ assert json.loads(bytes.fromhex(result['body'])) \
+ == {'counter': counter_initial}
tab_id = run_content_script_in_new_window(driver, 'https://gotmyowndoma.in')
result = fetch_through_cache(driver, tab_id, 'https://counterdoma.in/')
- assert result['json']['counter'] == counter_initial + 1
+ assert json.loads(bytes.fromhex(result['body'])) \
+ == {'counter': counter_initial + 1}
for i in range(2):
result = fetch_through_cache(driver, tab_id, 'https://nxdoma.in/')
- assert set(result.keys()) == {'ok', 'status', 'error_json'}
- assert result['ok'] == False
assert result['status'] == 404
for i in range(2):
result = fetch_through_cache(driver, tab_id, 'bad://url')
assert set(result.keys()) == {'error'}
+ assert result['error']['name'] == 'TypeError'
@pytest.mark.ext_data({
'content_script': content_script,
@@ -128,3 +129,7 @@ def test_repo_query_cacher_bgscript_error(driver):
result = fetch_through_cache(driver, tab_id, 'https://counterdoma.in/')
assert set(result.keys()) == {'error'}
+ assert set(result['error'].keys()) == \
+ {'name', 'message', 'fileName', 'lineNumber'}
+ assert result['error']['message'] == \
+ "Couldn't communicate with background script."
diff --git a/test/haketilo_test/unit/utils.py b/test/haketilo_test/unit/utils.py
index 7ddf92a..a49ce8c 100644
--- a/test/haketilo_test/unit/utils.py
+++ b/test/haketilo_test/unit/utils.py
@@ -246,36 +246,40 @@ def mock_broadcast(execute_in_page):
'Object.keys(broadcast).forEach(k => broadcast[k] = () => {});'
)
-def mock_cacher(execute_in_page):
- """
- Some parts of code depend on content/repo_query_cacher.js and
- background/CORS_bypass_server.js running in their appropriate contexts. This
- function modifies the relevant browser.runtime.sendMessage function to
- perform fetch(), bypassing the cacher.
- """
- execute_in_page(
- '''{
- const old_sendMessage = browser.tabs.sendMessage, old_fetch = fetch;
- async function new_sendMessage(tab_id, msg) {
- if (msg[0] !== "repo_query")
- return old_sendMessage(tab_id, msg);
+"""
+Some parts of code depend on content/repo_query_cacher.js and
+background/CORS_bypass_server.js running in their appropriate contexts. This
+snippet modifies the relevant browser.runtime.sendMessage function to perform
+fetch(), bypassing the cacher.
+"""
+mock_cacher_code = '''{
+const uint8_to_hex =
+ array => [...array].map(b => ("0" + b.toString(16)).slice(-2)).join("");
- /* Use snapshotted fetch(), allow other test code to override it. */
- const response = await old_fetch(msg[1]);
- if (!response)
- return {error: "Something happened :o"};
+const old_sendMessage = browser.tabs.sendMessage;
+window.mock_cacher_fetch = fetch;
+browser.tabs.sendMessage = async function(tab_id, msg) {
+ if (msg[0] !== "repo_query")
+ return old_sendMessage(tab_id, msg);
- const result = {ok: response.ok, status: response.status};
- try {
- result.json = await response.json();
- } catch(e) {
- result.error_json = "" + e;
- }
- return result;
+ /*
+ * Use snapshotted fetch() under the name window.mock_cacher_fetch,
+ * allow other test code to override it.
+ */
+ try {
+ const response = await window.mock_cacher_fetch(msg[1]);
+ const buf = await response.arrayBuffer();
+ return {
+ status: response.status,
+ statusText: response.statusText,
+ headers: [...response.headers.entries()],
+ body: uint8_to_hex(new Uint8Array(buf))
}
-
- browser.tabs.sendMessage = new_sendMessage;
- }''')
+ } catch(e) {
+ return {error: {name: e.name, message: e.message}};
+ }
+}
+}'''
"""
Convenience snippet of code to retrieve a copy of given object with only those