tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 });