test_suspendable_channel_wrapper.js (8411B)
1 /* Any copyright is dedicated to the Public Domain. 2 https://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 const { sinon } = ChromeUtils.importESModule( 7 "resource://testing-common/Sinon.sys.mjs" 8 ); 9 10 const { setTimeout } = ChromeUtils.importESModule( 11 "resource://gre/modules/Timer.sys.mjs" 12 ); 13 14 /** 15 * A very basic nsIChannel implementation in JavaScript that we can spy on 16 * and stub methods on using Sinon. 17 */ 18 class MockChannel { 19 #uri = null; 20 21 constructor(uriString) { 22 let uri = Services.io.newURI(uriString); 23 this.#uri = uri; 24 this.originalURI = uri; 25 } 26 27 contentType = "application/x-mock-channel-content"; 28 loadAttributes = null; 29 contentLength = 0; 30 owner = null; 31 notificationCallbacks = null; 32 securityInfo = null; 33 originalURI = null; 34 status = Cr.NS_OK; 35 36 get name() { 37 return this.#uri; 38 } 39 40 get URI() { 41 return this.#uri; 42 } 43 44 get loadGroup() { 45 return null; 46 } 47 set loadGroup(_val) {} 48 49 get loadInfo() { 50 return null; 51 } 52 set loadInfo(_val) {} 53 54 open() { 55 throw Components.Exception( 56 `${this.constructor.name}.open not implemented`, 57 Cr.NS_ERROR_NOT_IMPLEMENTED 58 ); 59 } 60 61 asyncOpen(observer) { 62 observer.onStartRequest(this, null); 63 } 64 65 asyncRead(listener, ctxt) { 66 return listener.onStartRequest(this, ctxt); 67 } 68 69 isPending() { 70 return false; 71 } 72 73 cancel(status) { 74 this.status = status; 75 } 76 77 suspend() { 78 throw Components.Exception( 79 `${this.constructor.name}.suspend not implemented`, 80 Cr.NS_ERROR_NOT_IMPLEMENTED 81 ); 82 } 83 84 resume() { 85 throw Components.Exception( 86 `${this.constructor.name}.resume not implemented`, 87 Cr.NS_ERROR_NOT_IMPLEMENTED 88 ); 89 } 90 91 QueryInterface = ChromeUtils.generateQI([ 92 "nsIChannel", 93 "nsIRequest", 94 // We obviously don't implement nsIRegion here, but we want to test that we 95 // can QI down to whatever the inner channel implements. 96 "nsIRegion", 97 ]); 98 } 99 100 /** 101 * A bare-minimum nsIStreamListener that doesn't do anything, useful for 102 * passing into methods that expect one of these. 103 */ 104 class FakeStreamListener { 105 onStartRequest(_request) {} 106 onDataAvailable(_request, _stream, _offset, _count) {} 107 onStopRequest(_request, _status) {} 108 QueryInterface = ChromeUtils.generateQI(["nsIStreamListener"]); 109 } 110 111 /** 112 * Test that calling asyncOpen on a nsISuspendedChannel does not call 113 * asyncOpen on the inner channel initially if the nsISuspendedChannel had 114 * been suspended. Only after calling resume() on the nsISuspendedChannel does 115 * the asyncOpen call go through. 116 */ 117 add_task(async function test_no_asyncOpen_inner() { 118 let innerChannel = new MockChannel("about:newtab"); 119 Assert.ok(innerChannel.QueryInterface(Ci.nsIChannel)); 120 let suspendedChannel = Services.io.newSuspendableChannelWrapper(innerChannel); 121 122 let sandbox = sinon.createSandbox(); 123 sandbox.stub(innerChannel, "asyncOpen"); 124 125 suspendedChannel.suspend(); 126 127 let fakeStreamListener = new FakeStreamListener(); 128 suspendedChannel.asyncOpen(fakeStreamListener); 129 Assert.ok(innerChannel.asyncOpen.notCalled, "asyncOpen not called on inner"); 130 Assert.ok(suspendedChannel.isPending(), "suspended channel is pending"); 131 suspendedChannel.resume(); 132 Assert.ok(innerChannel.asyncOpen.calledOnce, "asyncOpen called on inner"); 133 134 sandbox.restore(); 135 }); 136 137 /** 138 * Tests that nsIChannel and nsIRequest property and method calls are 139 * forwarded to the inner channel (except for asyncOpen). This isn't really 140 * exhaustive, but checks some fairly important methods and properties. 141 */ 142 add_task(async function test_forwarding() { 143 let innerChannel = new MockChannel("about:newtab"); 144 let suspendedChannel = Services.io.newSuspendableChannelWrapper(innerChannel); 145 146 let sandbox = sinon.createSandbox(); 147 sandbox.stub(innerChannel, "asyncOpen"); 148 149 let nameSpy = sandbox.spy(innerChannel, "name", ["get"]); 150 suspendedChannel.name; 151 Assert.ok(nameSpy.get.calledOnce, "name was retreived from inner"); 152 153 sandbox.stub(innerChannel, "suspend"); 154 suspendedChannel.suspend(); 155 Assert.ok( 156 innerChannel.suspend.notCalled, 157 "suspend not called on inner (since not yet opened)" 158 ); 159 160 sandbox.stub(innerChannel, "resume"); 161 suspendedChannel.resume(); 162 Assert.ok( 163 innerChannel.resume.notCalled, 164 "resume not called on inner (since not yet opened)" 165 ); 166 167 let loadGroupSpy = sandbox.spy(innerChannel, "loadGroup", ["get", "set"]); 168 suspendedChannel.loadGroup; 169 Assert.ok(loadGroupSpy.get.calledOnce, "loadGroup was retreived from inner"); 170 suspendedChannel.loadGroup = null; 171 Assert.ok(loadGroupSpy.set.calledOnce, "loadGroup was set on inner"); 172 173 let loadInfoSpy = sandbox.spy(innerChannel, "loadInfo", ["get", "set"]); 174 suspendedChannel.loadInfo; 175 Assert.ok(loadInfoSpy.get.calledOnce, "loadInfo was retreived from inner"); 176 suspendedChannel.loadInfo = null; 177 Assert.ok(loadInfoSpy.set.calledOnce, "loadInfo was set on inner"); 178 179 let URISpy = sandbox.spy(innerChannel, "URI", ["get"]); 180 suspendedChannel.URI; 181 Assert.ok(URISpy.get.calledOnce, "URI was retreived from inner"); 182 183 Assert.ok( 184 innerChannel.asyncOpen.notCalled, 185 "asyncOpen never called on the inner channel" 186 ); 187 188 // Now check that QI forwarding works for the underlying channel. 189 Assert.ok( 190 innerChannel.QueryInterface(Ci.nsIRegion), 191 "Inner QIs to nsIRegion" 192 ); 193 194 Assert.ok( 195 suspendedChannel.QueryInterface(Ci.nsIRegion), 196 "Can QI to something the inner channel implements" 197 ); 198 199 sandbox.restore(); 200 }); 201 202 /** 203 * Test that calling resume on an nsISuspendedChannel does not call 204 * asyncOpen on the inner channel until asyncOpen is called on the 205 * nsISuspendedChannel. 206 */ 207 add_task(async function test_no_asyncOpen_on_resume() { 208 let innerChannel = new MockChannel("about:newtab"); 209 let suspendedChannel = Services.io.newSuspendableChannelWrapper(innerChannel); 210 suspendedChannel.suspend(); 211 212 let sandbox = sinon.createSandbox(); 213 sandbox.stub(innerChannel, "asyncOpen"); 214 215 Assert.ok(innerChannel.asyncOpen.notCalled, "asyncOpen not called on inner"); 216 Assert.ok(suspendedChannel.isPending(), "suspended channel is pending"); 217 suspendedChannel.resume(); 218 Assert.ok(innerChannel.asyncOpen.notCalled, "asyncOpen not called on inner"); 219 220 let fakeStreamListener = new FakeStreamListener(); 221 suspendedChannel.asyncOpen(fakeStreamListener); 222 Assert.ok(innerChannel.asyncOpen.calledOnce, "asyncOpen called on inner"); 223 224 sandbox.restore(); 225 }); 226 227 /** 228 * Test that we can get access to the data provided by the inner channel through 229 * an nsISuspendedChannel that has been resumed after being suspended. 230 */ 231 add_task(async function test_allow_data() { 232 let innerChannel = Cc["@mozilla.org/network/input-stream-channel;1"] 233 .createInstance(Ci.nsIInputStreamChannel) 234 .QueryInterface(Ci.nsIChannel); 235 let suspendedChannel = Services.io.newSuspendableChannelWrapper(innerChannel); 236 237 const TEST_STRING = "This is a test string!"; 238 let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance( 239 Ci.nsIStringInputStream 240 ); 241 stringStream.setByteStringData(TEST_STRING); 242 243 // Let's just make up some HTTPChannel to steal some properties from to 244 // make things easier. 245 let httpChan = NetUtil.newChannel({ 246 uri: "http://localhost", 247 loadUsingSystemPrincipal: true, 248 }); 249 innerChannel.contentStream = stringStream; 250 innerChannel.contentType = "text/plain"; 251 innerChannel.setURI(httpChan.URI); 252 innerChannel.loadInfo = httpChan.loadInfo; 253 254 suspendedChannel.suspend(); 255 256 let completedFetch = false; 257 let fetchPromise = new Promise((resolve, reject) => { 258 NetUtil.asyncFetch(suspendedChannel, (stream, result) => { 259 if (!Components.isSuccessCode(result)) { 260 reject(new Error(`Failed to fetch stream`)); 261 return; 262 } 263 completedFetch = true; 264 265 resolve(stream); 266 }); 267 }); 268 269 // Wait for 1 second to make sure that the fetch didn't occur. 270 // eslint-disable-next-line mozilla/no-arbitrary-setTimeout 271 await new Promise(resolve => setTimeout(resolve, 1000)); 272 Assert.ok(!completedFetch, "Should not have completed the fetch."); 273 274 suspendedChannel.resume(); 275 let resultStream = await fetchPromise; 276 Assert.ok(completedFetch, "Should have completed the fetch."); 277 278 let resultString = NetUtil.readInputStreamToString( 279 resultStream, 280 resultStream.available() 281 ); 282 283 Assert.equal(TEST_STRING, resultString, "Got back the expected string."); 284 });