summaryrefslogtreecommitdiffstats
path: root/dom/serviceworkers/test/browser_devtools_serviceworker_interception.js
blob: f31c2f7dff0cb3e5c31c6d45a3c69436fbbd2e43 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
"use strict";

const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/";
const emptyDoc = BASE_URI + "empty.html";
const fakeDoc = BASE_URI + "fake.html";
const helloDoc = BASE_URI + "hello.html";

const CROSS_URI = "http://example.com/browser/dom/serviceworkers/test/";
const crossRedirect = CROSS_URI + "redirect";
const crossHelloDoc = CROSS_URI + "hello.html";

const sw = BASE_URI + "fetch.js";

async function checkObserver(aInput) {
  let interceptedChannel = null;

  // We always get two channels which receive the "http-on-stop-request"
  // notification if the service worker hijacks the request and respondWith an
  // another fetch. One is for the "outer" window request when the other one is
  // for the "inner" service worker request. Therefore, distinguish them by the
  // order.
  let waitForSecondOnStopRequest = aInput.intercepted;

  let promiseResolve;

  function observer(aSubject) {
    let channel = aSubject.QueryInterface(Ci.nsIChannel);
    // Since we cannot make sure that the network event triggered by the fetch()
    // in this testcase is the very next event processed by ObserverService, we
    // have to wait until we catch the one we want.
    if (!channel.URI.spec.includes(aInput.expectedURL)) {
      return;
    }

    if (waitForSecondOnStopRequest) {
      waitForSecondOnStopRequest = false;
      return;
    }

    // Wait for the service worker to intercept the request if it's expected to
    // be intercepted
    if (aInput.intercepted && interceptedChannel === null) {
      return;
    } else if (interceptedChannel) {
      ok(
        aInput.intercepted,
        "Service worker intercepted the channel as expected"
      );
    } else {
      ok(!aInput.intercepted, "The channel doesn't be intercepted");
    }

    var tc = interceptedChannel
      ? interceptedChannel.QueryInterface(Ci.nsITimedChannel)
      : aSubject.QueryInterface(Ci.nsITimedChannel);

    // Check service worker related timings.
    var serviceWorkerTimings = [
      {
        start: tc.launchServiceWorkerStartTime,
        end: tc.launchServiceWorkerEndTime,
      },
      {
        start: tc.dispatchFetchEventStartTime,
        end: tc.dispatchFetchEventEndTime,
      },
      { start: tc.handleFetchEventStartTime, end: tc.handleFetchEventEndTime },
    ];
    if (!aInput.swPresent) {
      serviceWorkerTimings.forEach(aTimings => {
        is(aTimings.start, 0, "SW timings should be 0.");
        is(aTimings.end, 0, "SW timings should be 0.");
      });
    }

    // Check network related timings.
    var networkTimings = [
      tc.domainLookupStartTime,
      tc.domainLookupEndTime,
      tc.connectStartTime,
      tc.connectEndTime,
      tc.requestStartTime,
      tc.responseStartTime,
      tc.responseEndTime,
    ];
    if (aInput.fetch) {
      networkTimings.reduce((aPreviousTiming, aCurrentTiming) => {
        Assert.lessOrEqual(
          aPreviousTiming,
          aCurrentTiming,
          "Checking network timings"
        );
        return aCurrentTiming;
      });
    } else {
      networkTimings.forEach(aTiming =>
        is(aTiming, 0, "Network timings should be 0.")
      );
    }

    interceptedChannel = null;
    Services.obs.removeObserver(observer, topic);
    promiseResolve();
  }

  function addInterceptedChannel(aSubject) {
    let channel = aSubject.QueryInterface(Ci.nsIChannel);
    if (!channel.URI.spec.includes(aInput.url)) {
      return;
    }

    // Hold the interceptedChannel until checking timing information.
    // Note: It's a interceptedChannel in the type of httpChannel
    interceptedChannel = channel;
    Services.obs.removeObserver(addInterceptedChannel, topic_SW);
  }

  const topic = "http-on-stop-request";
  const topic_SW = "service-worker-synthesized-response";

  Services.obs.addObserver(observer, topic);
  if (aInput.intercepted) {
    Services.obs.addObserver(addInterceptedChannel, topic_SW);
  }

  await new Promise(resolve => {
    promiseResolve = resolve;
  });
}

async function contentFetch(aURL) {
  if (aURL.includes("redirect")) {
    await content.window.fetch(aURL, { mode: "no-cors" });
    return;
  }
  await content.window.fetch(aURL);
}

// The observer topics are fired in the parent process in parent-intercept
// and the content process in child-intercept. This function will handle running
// the check in the correct process. Note that it will block until the observers
// are notified.
async function fetchAndCheckObservers(
  aFetchBrowser,
  aObserverBrowser,
  aTestCase
) {
  let promise = null;

  promise = checkObserver(aTestCase);

  await SpecialPowers.spawn(aFetchBrowser, [aTestCase.url], contentFetch);
  await promise;
}

async function registerSWAndWaitForActive(aServiceWorker) {
  let swr = await content.navigator.serviceWorker.register(aServiceWorker, {
    scope: "empty.html",
  });
  await new Promise(resolve => {
    let worker = swr.installing || swr.waiting || swr.active;
    if (worker.state === "activated") {
      resolve();
      return;
    }

    worker.addEventListener("statechange", () => {
      if (worker.state === "activated") {
        resolve();
      }
    });
  });

  await new Promise(resolve => {
    if (content.navigator.serviceWorker.controller) {
      resolve();
      return;
    }

    content.navigator.serviceWorker.addEventListener(
      "controllerchange",
      resolve,
      { once: true }
    );
  });
}

async function unregisterSW() {
  let swr = await content.navigator.serviceWorker.getRegistration();
  swr.unregister();
}

add_task(async function test_serivce_worker_interception() {
  info("Setting the prefs to having e10s enabled");
  await SpecialPowers.pushPrefEnv({
    set: [
      // Make sure observer and testing function run in the same process
      ["dom.ipc.processCount", 1],
      ["dom.serviceWorkers.enabled", true],
      ["dom.serviceWorkers.testing.enabled", true],
    ],
  });

  waitForExplicitFinish();

  info("Open the tab");
  let tab = BrowserTestUtils.addTab(gBrowser, emptyDoc);
  let tabBrowser = gBrowser.getBrowserForTab(tab);
  await BrowserTestUtils.browserLoaded(tabBrowser);

  info("Open the tab for observing");
  let tab_observer = BrowserTestUtils.addTab(gBrowser, emptyDoc);
  let tabBrowser_observer = gBrowser.getBrowserForTab(tab_observer);
  await BrowserTestUtils.browserLoaded(tabBrowser_observer);

  let testcases = [
    {
      url: helloDoc,
      expectedURL: helloDoc,
      swPresent: false,
      intercepted: false,
      fetch: true,
    },
    {
      url: fakeDoc,
      expectedURL: helloDoc,
      swPresent: true,
      intercepted: true,
      fetch: false, // should use HTTP cache
    },
    {
      // Bypass http cache
      url: helloDoc + "?ForBypassingHttpCache=" + Date.now(),
      expectedURL: helloDoc,
      swPresent: true,
      intercepted: false,
      fetch: true,
    },
    {
      // no-cors mode redirect to no-cors mode (trigger internal redirect)
      url: crossRedirect + "?url=" + crossHelloDoc + "&mode=no-cors",
      expectedURL: crossHelloDoc,
      swPresent: true,
      redirect: "hello.html",
      intercepted: true,
      fetch: true,
    },
  ];

  info("Test 1: Verify simple fetch");
  await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[0]);

  info("Register a service worker");
  await SpecialPowers.spawn(tabBrowser, [sw], registerSWAndWaitForActive);

  info("Test 2: Verify simple hijack");
  await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[1]);

  info("Test 3: Verify fetch without using http cache");
  await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[2]);

  info("Test 4: make a internal redirect");
  await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[3]);

  info("Clean up");
  await SpecialPowers.spawn(tabBrowser, [undefined], unregisterSW);

  gBrowser.removeTab(tab);
  gBrowser.removeTab(tab_observer);
});