284 lines
8.2 KiB
JavaScript
284 lines
8.2 KiB
JavaScript
/* Any copyright is dedicated to the Public Domain.
|
|
https://creativecommons.org/publicdomain/zero/1.0/ */
|
|
|
|
"use strict";
|
|
|
|
const { sinon } = ChromeUtils.importESModule(
|
|
"resource://testing-common/Sinon.sys.mjs"
|
|
);
|
|
|
|
const { setTimeout } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/Timer.sys.mjs"
|
|
);
|
|
|
|
/**
|
|
* A very basic nsIChannel implementation in JavaScript that we can spy on
|
|
* and stub methods on using Sinon.
|
|
*/
|
|
class MockChannel {
|
|
#uri = null;
|
|
|
|
constructor(uriString) {
|
|
let uri = Services.io.newURI(uriString);
|
|
this.#uri = uri;
|
|
this.originalURI = uri;
|
|
}
|
|
|
|
contentType = "application/x-mock-channel-content";
|
|
loadAttributes = null;
|
|
contentLength = 0;
|
|
owner = null;
|
|
notificationCallbacks = null;
|
|
securityInfo = null;
|
|
originalURI = null;
|
|
status = Cr.NS_OK;
|
|
|
|
get name() {
|
|
return this.#uri;
|
|
}
|
|
|
|
get URI() {
|
|
return this.#uri;
|
|
}
|
|
|
|
get loadGroup() {
|
|
return null;
|
|
}
|
|
set loadGroup(_val) {}
|
|
|
|
get loadInfo() {
|
|
return null;
|
|
}
|
|
set loadInfo(_val) {}
|
|
|
|
open() {
|
|
throw Components.Exception(
|
|
`${this.constructor.name}.open not implemented`,
|
|
Cr.NS_ERROR_NOT_IMPLEMENTED
|
|
);
|
|
}
|
|
|
|
asyncOpen(observer) {
|
|
observer.onStartRequest(this, null);
|
|
}
|
|
|
|
asyncRead(listener, ctxt) {
|
|
return listener.onStartRequest(this, ctxt);
|
|
}
|
|
|
|
isPending() {
|
|
return false;
|
|
}
|
|
|
|
cancel(status) {
|
|
this.status = status;
|
|
}
|
|
|
|
suspend() {
|
|
throw Components.Exception(
|
|
`${this.constructor.name}.suspend not implemented`,
|
|
Cr.NS_ERROR_NOT_IMPLEMENTED
|
|
);
|
|
}
|
|
|
|
resume() {
|
|
throw Components.Exception(
|
|
`${this.constructor.name}.resume not implemented`,
|
|
Cr.NS_ERROR_NOT_IMPLEMENTED
|
|
);
|
|
}
|
|
|
|
QueryInterface = ChromeUtils.generateQI([
|
|
"nsIChannel",
|
|
"nsIRequest",
|
|
// We obviously don't implement nsIRegion here, but we want to test that we
|
|
// can QI down to whatever the inner channel implements.
|
|
"nsIRegion",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* A bare-minimum nsIStreamListener that doesn't do anything, useful for
|
|
* passing into methods that expect one of these.
|
|
*/
|
|
class FakeStreamListener {
|
|
onStartRequest(_request) {}
|
|
onDataAvailable(_request, _stream, _offset, _count) {}
|
|
onStopRequest(_request, _status) {}
|
|
QueryInterface = ChromeUtils.generateQI(["nsIStreamListener"]);
|
|
}
|
|
|
|
/**
|
|
* Test that calling asyncOpen on a nsISuspendedChannel does not call
|
|
* asyncOpen on the inner channel initially if the nsISuspendedChannel had
|
|
* been suspended. Only after calling resume() on the nsISuspendedChannel does
|
|
* the asyncOpen call go through.
|
|
*/
|
|
add_task(async function test_no_asyncOpen_inner() {
|
|
let innerChannel = new MockChannel("about:newtab");
|
|
Assert.ok(innerChannel.QueryInterface(Ci.nsIChannel));
|
|
let suspendedChannel = Services.io.newSuspendableChannelWrapper(innerChannel);
|
|
|
|
let sandbox = sinon.createSandbox();
|
|
sandbox.stub(innerChannel, "asyncOpen");
|
|
|
|
suspendedChannel.suspend();
|
|
|
|
let fakeStreamListener = new FakeStreamListener();
|
|
suspendedChannel.asyncOpen(fakeStreamListener);
|
|
Assert.ok(innerChannel.asyncOpen.notCalled, "asyncOpen not called on inner");
|
|
Assert.ok(suspendedChannel.isPending(), "suspended channel is pending");
|
|
suspendedChannel.resume();
|
|
Assert.ok(innerChannel.asyncOpen.calledOnce, "asyncOpen called on inner");
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
/**
|
|
* Tests that nsIChannel and nsIRequest property and method calls are
|
|
* forwarded to the inner channel (except for asyncOpen). This isn't really
|
|
* exhaustive, but checks some fairly important methods and properties.
|
|
*/
|
|
add_task(async function test_forwarding() {
|
|
let innerChannel = new MockChannel("about:newtab");
|
|
let suspendedChannel = Services.io.newSuspendableChannelWrapper(innerChannel);
|
|
|
|
let sandbox = sinon.createSandbox();
|
|
sandbox.stub(innerChannel, "asyncOpen");
|
|
|
|
let nameSpy = sandbox.spy(innerChannel, "name", ["get"]);
|
|
suspendedChannel.name;
|
|
Assert.ok(nameSpy.get.calledOnce, "name was retreived from inner");
|
|
|
|
sandbox.stub(innerChannel, "suspend");
|
|
suspendedChannel.suspend();
|
|
Assert.ok(
|
|
innerChannel.suspend.notCalled,
|
|
"suspend not called on inner (since not yet opened)"
|
|
);
|
|
|
|
sandbox.stub(innerChannel, "resume");
|
|
suspendedChannel.resume();
|
|
Assert.ok(
|
|
innerChannel.resume.notCalled,
|
|
"resume not called on inner (since not yet opened)"
|
|
);
|
|
|
|
let loadGroupSpy = sandbox.spy(innerChannel, "loadGroup", ["get", "set"]);
|
|
suspendedChannel.loadGroup;
|
|
Assert.ok(loadGroupSpy.get.calledOnce, "loadGroup was retreived from inner");
|
|
suspendedChannel.loadGroup = null;
|
|
Assert.ok(loadGroupSpy.set.calledOnce, "loadGroup was set on inner");
|
|
|
|
let loadInfoSpy = sandbox.spy(innerChannel, "loadInfo", ["get", "set"]);
|
|
suspendedChannel.loadInfo;
|
|
Assert.ok(loadInfoSpy.get.calledOnce, "loadInfo was retreived from inner");
|
|
suspendedChannel.loadInfo = null;
|
|
Assert.ok(loadInfoSpy.set.calledOnce, "loadInfo was set on inner");
|
|
|
|
let URISpy = sandbox.spy(innerChannel, "URI", ["get"]);
|
|
suspendedChannel.URI;
|
|
Assert.ok(URISpy.get.calledOnce, "URI was retreived from inner");
|
|
|
|
Assert.ok(
|
|
innerChannel.asyncOpen.notCalled,
|
|
"asyncOpen never called on the inner channel"
|
|
);
|
|
|
|
// Now check that QI forwarding works for the underlying channel.
|
|
Assert.ok(
|
|
innerChannel.QueryInterface(Ci.nsIRegion),
|
|
"Inner QIs to nsIRegion"
|
|
);
|
|
|
|
Assert.ok(
|
|
suspendedChannel.QueryInterface(Ci.nsIRegion),
|
|
"Can QI to something the inner channel implements"
|
|
);
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
/**
|
|
* Test that calling resume on an nsISuspendedChannel does not call
|
|
* asyncOpen on the inner channel until asyncOpen is called on the
|
|
* nsISuspendedChannel.
|
|
*/
|
|
add_task(async function test_no_asyncOpen_on_resume() {
|
|
let innerChannel = new MockChannel("about:newtab");
|
|
let suspendedChannel = Services.io.newSuspendableChannelWrapper(innerChannel);
|
|
suspendedChannel.suspend();
|
|
|
|
let sandbox = sinon.createSandbox();
|
|
sandbox.stub(innerChannel, "asyncOpen");
|
|
|
|
Assert.ok(innerChannel.asyncOpen.notCalled, "asyncOpen not called on inner");
|
|
Assert.ok(suspendedChannel.isPending(), "suspended channel is pending");
|
|
suspendedChannel.resume();
|
|
Assert.ok(innerChannel.asyncOpen.notCalled, "asyncOpen not called on inner");
|
|
|
|
let fakeStreamListener = new FakeStreamListener();
|
|
suspendedChannel.asyncOpen(fakeStreamListener);
|
|
Assert.ok(innerChannel.asyncOpen.calledOnce, "asyncOpen called on inner");
|
|
|
|
sandbox.restore();
|
|
});
|
|
|
|
/**
|
|
* Test that we can get access to the data provided by the inner channel through
|
|
* an nsISuspendedChannel that has been resumed after being suspended.
|
|
*/
|
|
add_task(async function test_allow_data() {
|
|
let innerChannel = Cc["@mozilla.org/network/input-stream-channel;1"]
|
|
.createInstance(Ci.nsIInputStreamChannel)
|
|
.QueryInterface(Ci.nsIChannel);
|
|
let suspendedChannel = Services.io.newSuspendableChannelWrapper(innerChannel);
|
|
|
|
const TEST_STRING = "This is a test string!";
|
|
let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
|
|
Ci.nsIStringInputStream
|
|
);
|
|
stringStream.setByteStringData(TEST_STRING);
|
|
|
|
// Let's just make up some HTTPChannel to steal some properties from to
|
|
// make things easier.
|
|
let httpChan = NetUtil.newChannel({
|
|
uri: "http://localhost",
|
|
loadUsingSystemPrincipal: true,
|
|
});
|
|
innerChannel.contentStream = stringStream;
|
|
innerChannel.contentType = "text/plain";
|
|
innerChannel.setURI(httpChan.URI);
|
|
innerChannel.loadInfo = httpChan.loadInfo;
|
|
|
|
suspendedChannel.suspend();
|
|
|
|
let completedFetch = false;
|
|
let fetchPromise = new Promise((resolve, reject) => {
|
|
NetUtil.asyncFetch(suspendedChannel, (stream, result) => {
|
|
if (!Components.isSuccessCode(result)) {
|
|
reject(new Error(`Failed to fetch stream`));
|
|
return;
|
|
}
|
|
completedFetch = true;
|
|
|
|
resolve(stream);
|
|
});
|
|
});
|
|
|
|
// Wait for 1 second to make sure that the fetch didn't occur.
|
|
// eslint-disable-next-line mozilla/no-arbitrary-setTimeout
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
Assert.ok(!completedFetch, "Should not have completed the fetch.");
|
|
|
|
suspendedChannel.resume();
|
|
let resultStream = await fetchPromise;
|
|
Assert.ok(completedFetch, "Should have completed the fetch.");
|
|
|
|
let resultString = NetUtil.readInputStreamToString(
|
|
resultStream,
|
|
resultStream.available()
|
|
);
|
|
|
|
Assert.equal(TEST_STRING, resultString, "Got back the expected string.");
|
|
});
|