Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions app/src/androidTest/assets/test_requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,32 @@ function triggerFetchStringUrl() {
});
}

// Headers passed as a Headers instance — exercises normalizeFetchHeaders()
function triggerFetchStringUrlWithHeadersInstance() {
fetch('https://example.com/api/headers-instance', {
method: 'GET',
headers: new Headers({
'X-From-Instance': 'instance-value'
})
});
}

// Headers passed as an array of [name, value] tuples — exercises normalizeFetchHeaders()
function triggerFetchStringUrlWithHeadersArray() {
fetch('https://example.com/api/headers-array', {
method: 'GET',
headers: [
['X-From-Array', 'array-value']
]
});
}

// No options object at all — exercises the arguments-scope fix: arguments[1] starts
// as undefined and is set to {} before fetchOptions captures it.
function triggerFetchStringUrlNoOptions() {
fetch('https://example.com/api/no-options');
}

function triggerFetchRequestObject() {
fetch(new Request('https://example.com/api/request', {
method: 'PUT',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,73 @@ class JavaScriptInterfaceTest {
assertTrue("Expected URL to contain 'debug=true', was: $url", url.contains("debug=true"))
}

// - fetch (string URL, non-plain-object headers) ---------------

@Test
fun fetch_stringUrl_headersInstance_recordsHeaders() {
// normalizeFetchHeaders: Headers instance should be converted to a plain object
val latch = matcher.expectRequest()
runJs("triggerFetchStringUrlWithHeadersInstance()")
assertTrue("fetch (Headers instance) not recorded", latch.await(5, TimeUnit.SECONDS))

val req = matcher.lastRequest!!
assertEquals(WebViewRequestType.FETCH, req.type)
assertEquals("https://example.com/api/headers-instance", req.url.toString())
assertEquals("instance-value", req.headers["x-from-instance"])
}

@Test
fun fetch_stringUrl_headersInstance_injectsAdditionalHeaders() {
// normalizeFetchHeaders + arguments-scope fix: injection must work when headers is a Headers instance
matcher.additionalHeaders = mapOf("X-Injected" to "injected-value")
val latch = matcher.expectRequest()
runJs("triggerFetchStringUrlWithHeadersInstance()")
assertTrue("fetch (Headers instance) with extra header not recorded", latch.await(5, TimeUnit.SECONDS))

val req = matcher.lastRequest!!
assertEquals("instance-value", req.headers["x-from-instance"])
assertEquals("injected-value", req.headers["x-injected"])
}

@Test
fun fetch_stringUrl_headersArray_recordsHeaders() {
// normalizeFetchHeaders: array of [name, value] tuples should be converted to a plain object
val latch = matcher.expectRequest()
runJs("triggerFetchStringUrlWithHeadersArray()")
assertTrue("fetch (headers array) not recorded", latch.await(5, TimeUnit.SECONDS))

val req = matcher.lastRequest!!
assertEquals(WebViewRequestType.FETCH, req.type)
assertEquals("https://example.com/api/headers-array", req.url.toString())
assertEquals("array-value", req.headers["x-from-array"])
}

@Test
fun fetch_stringUrl_headersArray_injectsAdditionalHeaders() {
// normalizeFetchHeaders + arguments-scope fix: injection must work when headers is an array
matcher.additionalHeaders = mapOf("X-Injected" to "injected-value")
val latch = matcher.expectRequest()
runJs("triggerFetchStringUrlWithHeadersArray()")
assertTrue("fetch (headers array) with extra header not recorded", latch.await(5, TimeUnit.SECONDS))

val req = matcher.lastRequest!!
assertEquals("array-value", req.headers["x-from-array"])
assertEquals("injected-value", req.headers["x-injected"])
}

@Test
fun fetch_stringUrl_noOptions_injectsAdditionalHeaders() {
// arguments-scope fix: when fetch is called with no options object, arguments[1] starts as
// undefined and is initialised to {} in the override — fetchOptions must capture that new
// object so header injection does not throw inside the callback.
matcher.additionalHeaders = mapOf("X-Injected" to "injected-value")
val latch = matcher.expectRequest()
runJs("triggerFetchStringUrlNoOptions()")
assertTrue("fetch (no options) with extra header not recorded", latch.await(5, TimeUnit.SECONDS))

assertEquals("injected-value", matcher.lastRequest!!.headers["x-injected"])
}

// - fetch (request object) -------------------------------------

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,15 @@ function getFullUrl(url) {
}
}

// Normalizes the three forms fetch() accepts for headers — a Headers instance,
// an array of [name, value] tuples, or a plain object — into a plain object
// so it can be safely merged with the spread operator.
function normalizeFetchHeaders(headers) {
if (typeof Headers !== 'undefined' && headers instanceof Headers) return Object.fromEntries(headers.entries());
if (Array.isArray(headers)) return Object.fromEntries(headers);
return headers || {};
}

function setAdditionalHeaders(url, callback) {
try {
var extraHeaders = JSON.parse($INTERFACE_NAME.getAdditionalHeaders(url));
Expand Down Expand Up @@ -375,10 +384,18 @@ window.fetch = function () {
if (!arguments[1]) arguments[1] = {};
method = 'method' in arguments[1] ? arguments[1]['method'] : "GET";
body = 'body' in arguments[1] ? arguments[1]['body'] : "";
// Normalize headers up-front so JSON.stringify(headers) records correctly
// even if setAdditionalHeaders throws before invoking the callback.
headers = 'headers' in arguments[1] ? arguments[1]['headers'] : {};
headers = normalizeFetchHeaders(headers);
// Capture the options object here — inside the callback `arguments` is
// rebound to the callback's own argument list, so arguments[1] would be undefined.
var fetchOptions = arguments[1];
setAdditionalHeaders(url, function(extraHeaders) {
headers = { ...extraHeaders, ...headers };
arguments[1].headers = headers;
// extraHeaders are applied first so that any headers already set by
// the caller take precedence over the injected ones.
fetchOptions.headers = { ...extraHeaders, ...headers };
headers = fetchOptions.headers;
});
arguments[0] = url;
} else {
Expand Down