| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 var getURL = chrome.extension.getURL; |
| 6 var deepEq = chrome.test.checkDeepEq; |
| 7 var expectedEventData; |
| 8 var capturedEventData; |
| 9 var capturedUnexpectedData; |
| 10 var expectedEventOrder; |
| 11 var tabId; |
| 12 var tabIdMap; |
| 13 var frameIdMap; |
| 14 var testServerPort; |
| 15 var testServer = "www.a.com"; |
| 16 var defaultScheme = "http"; |
| 17 var eventsCaptured; |
| 18 var listeners = { |
| 19 'onBeforeRequest': [], |
| 20 'onBeforeSendHeaders': [], |
| 21 'onAuthRequired': [], |
| 22 'onSendHeaders': [], |
| 23 'onHeadersReceived': [], |
| 24 'onResponseStarted': [], |
| 25 'onBeforeRedirect': [], |
| 26 'onCompleted': [], |
| 27 'onErrorOccurred': [] |
| 28 }; |
| 29 |
| 30 // If true, don't bark on events that were not registered via expect(). |
| 31 // These events are recorded in capturedUnexpectedData instead of |
| 32 // capturedEventData. |
| 33 var ignoreUnexpected = false; |
| 34 |
| 35 // This is a debugging aid to print all received events as well as the |
| 36 // information whether they were expected. |
| 37 var logAllRequests = false; |
| 38 |
| 39 function runTests(tests) { |
| 40 var waitForAboutBlank = function(_, info, tab) { |
| 41 if (info.status == "complete" && tab.url == "about:blank") { |
| 42 tabId = tab.id; |
| 43 tabIdMap = {"-1": -1}; |
| 44 tabIdMap[tabId] = 0; |
| 45 chrome.tabs.onUpdated.removeListener(waitForAboutBlank); |
| 46 chrome.test.getConfig(function(config) { |
| 47 testServerPort = config.testServer.port; |
| 48 chrome.test.runTests(tests); |
| 49 }); |
| 50 } |
| 51 }; |
| 52 chrome.tabs.onUpdated.addListener(waitForAboutBlank); |
| 53 chrome.tabs.create({url: "about:blank"}); |
| 54 } |
| 55 |
| 56 // Returns an URL from the test server, fixing up the port. Must be called |
| 57 // from within a test case passed to runTests. |
| 58 function getServerURL(path, opt_host, opt_scheme) { |
| 59 if (!testServerPort) |
| 60 throw new Error("Called getServerURL outside of runTests."); |
| 61 var host = opt_host || testServer; |
| 62 var scheme = opt_scheme || defaultScheme; |
| 63 return scheme + "://" + host + ":" + testServerPort + "/" + path; |
| 64 } |
| 65 |
| 66 // Helper to advance to the next test only when the tab has finished loading. |
| 67 // This is because tabs.update can sometimes fail if the tab is in the middle |
| 68 // of a navigation (from the previous test), resulting in flakiness. |
| 69 function navigateAndWait(url, callback) { |
| 70 var done = chrome.test.listenForever(chrome.tabs.onUpdated, |
| 71 function (_, info, tab) { |
| 72 if (tab.id == tabId && info.status == "complete") { |
| 73 if (callback) callback(); |
| 74 done(); |
| 75 } |
| 76 }); |
| 77 chrome.tabs.update(tabId, {url: url}); |
| 78 } |
| 79 |
| 80 // data: array of extected events, each one is a dictionary: |
| 81 // { label: "<unique identifier>", |
| 82 // event: "<webrequest event type>", |
| 83 // details: { <expected details of the webrequest event> }, |
| 84 // retval: { <dictionary that the event handler shall return> } (optional) |
| 85 // } |
| 86 // order: an array of sequences, e.g. [ ["a", "b", "c"], ["d", "e"] ] means that |
| 87 // event with label "a" needs to occur before event with label "b". The |
| 88 // relative order of "a" and "d" does not matter. |
| 89 // filter: filter dictionary passed on to the event subscription of the |
| 90 // webRequest API. |
| 91 // extraInfoSpec: the union of all desired extraInfoSpecs for the events. |
| 92 function expect(data, order, filter, extraInfoSpec) { |
| 93 expectedEventData = data || []; |
| 94 capturedEventData = []; |
| 95 capturedUnexpectedData = []; |
| 96 expectedEventOrder = order || []; |
| 97 if (expectedEventData.length > 0) { |
| 98 eventsCaptured = chrome.test.callbackAdded(); |
| 99 } |
| 100 tabAndFrameUrls = {}; // Maps "{tabId}-{frameId}" to the URL of the frame. |
| 101 frameIdMap = {"-1": -1, "0": 0}; |
| 102 removeListeners(); |
| 103 initListeners(filter || {urls: ["<all_urls>"]}, extraInfoSpec || []); |
| 104 // Fill in default values. |
| 105 for (var i = 0; i < expectedEventData.length; ++i) { |
| 106 if (!('method' in expectedEventData[i].details)) { |
| 107 expectedEventData[i].details.method = "GET"; |
| 108 } |
| 109 if (!('tabId' in expectedEventData[i].details)) { |
| 110 expectedEventData[i].details.tabId = tabIdMap[tabId]; |
| 111 } |
| 112 if (!('frameId' in expectedEventData[i].details)) { |
| 113 expectedEventData[i].details.frameId = 0; |
| 114 } |
| 115 if (!('parentFrameId' in expectedEventData[i].details)) { |
| 116 expectedEventData[i].details.parentFrameId = -1; |
| 117 } |
| 118 if (!('type' in expectedEventData[i].details)) { |
| 119 expectedEventData[i].details.type = "main_frame"; |
| 120 } |
| 121 } |
| 122 } |
| 123 |
| 124 function checkExpectations() { |
| 125 if (capturedEventData.length < expectedEventData.length) { |
| 126 return; |
| 127 } |
| 128 if (capturedEventData.length > expectedEventData.length) { |
| 129 chrome.test.fail("Recorded too many events. " + |
| 130 JSON.stringify(capturedEventData)); |
| 131 return; |
| 132 } |
| 133 // We have ensured that capturedEventData contains exactly the same elements |
| 134 // as expectedEventData. Now we need to verify the ordering. |
| 135 // Step 1: build positions such that |
| 136 // positions[<event-label>]=<position of this event in capturedEventData> |
| 137 var curPos = 0; |
| 138 var positions = {} |
| 139 capturedEventData.forEach(function (event) { |
| 140 chrome.test.assertTrue(event.hasOwnProperty("label")); |
| 141 positions[event.label] = curPos; |
| 142 curPos++; |
| 143 }); |
| 144 // Step 2: check that elements arrived in correct order |
| 145 expectedEventOrder.forEach(function (order) { |
| 146 var previousLabel = undefined; |
| 147 order.forEach(function(label) { |
| 148 if (previousLabel === undefined) { |
| 149 previousLabel = label; |
| 150 return; |
| 151 } |
| 152 chrome.test.assertTrue(positions[previousLabel] < positions[label], |
| 153 "Event " + previousLabel + " is supposed to arrive before " + |
| 154 label + "."); |
| 155 previousLabel = label; |
| 156 }); |
| 157 }); |
| 158 |
| 159 eventsCaptured(); |
| 160 } |
| 161 |
| 162 // Simple check to see that we have a User-Agent header, and that it contains |
| 163 // an expected value. This is a basic check that the request headers are valid. |
| 164 function checkUserAgent(headers) { |
| 165 for (var i in headers) { |
| 166 if (headers[i].name.toLowerCase() == "user-agent") |
| 167 return headers[i].value.toLowerCase().indexOf("chrome") != -1; |
| 168 } |
| 169 return false; |
| 170 } |
| 171 |
| 172 // Whether the request is missing a tabId and frameId and we're not expecting |
| 173 // a request with the given details. If the method returns true, the event |
| 174 // should be ignored. |
| 175 function isUnexpectedDetachedRequest(name, details) { |
| 176 // This function is responsible for marking detached requests as unexpected. |
| 177 // Non-detached requests are not this function's concern. |
| 178 if (details.tabId !== -1 || details.frameId >= 0) |
| 179 return false; |
| 180 |
| 181 // Only return true if there is no matching expectation for the given details. |
| 182 return !expectedEventData.some(function(exp) { |
| 183 var didMatchTabAndFrameId = |
| 184 exp.details.tabId === -1 && |
| 185 exp.details.frameId === -1; |
| 186 |
| 187 // Accept non-matching tabId/frameId for ping/beacon requests because these |
| 188 // requests can continue after a frame is removed. And due to a bug, such |
| 189 // requests have a tabId/frameId of -1. |
| 190 // The test will fail anyway, but then with a helpful error (expectation |
| 191 // differs from actual events) instead of an obscure test timeout. |
| 192 // TODO(robwu): Remove this once https://crbug.com/522129 gets fixed. |
| 193 didMatchTabAndFrameId = didMatchTabAndFrameId || details.type === 'ping'; |
| 194 |
| 195 return name === exp.event && |
| 196 didMatchTabAndFrameId && |
| 197 exp.details.method === details.method && |
| 198 exp.details.url === details.url && |
| 199 exp.details.type === details.type; |
| 200 }); |
| 201 } |
| 202 |
| 203 function captureEvent(name, details, callback) { |
| 204 // Ignore system-level requests like safebrowsing updates and favicon fetches |
| 205 // since they are unpredictable. |
| 206 if ((details.type == "other" && !details.url.includes('dont-ignore-me')) || |
| 207 isUnexpectedDetachedRequest(name, details) || |
| 208 details.url.match(/\/favicon.ico$/) || |
| 209 details.url.match(/https:\/\/dl.google.com/)) |
| 210 return; |
| 211 |
| 212 // Pull the extra per-event options out of the expected data. These let |
| 213 // us specify special return values per event. |
| 214 var currentIndex = capturedEventData.length; |
| 215 var extraOptions; |
| 216 var retval; |
| 217 if (expectedEventData.length > currentIndex) { |
| 218 retval = |
| 219 expectedEventData[currentIndex].retval_function ? |
| 220 expectedEventData[currentIndex].retval_function(name, details) : |
| 221 expectedEventData[currentIndex].retval; |
| 222 } |
| 223 |
| 224 // Check that the frameId can be used to reliably determine the URL of the |
| 225 // frame that caused requests. |
| 226 if (name == "onBeforeRequest") { |
| 227 chrome.test.assertTrue('frameId' in details && |
| 228 typeof details.frameId === 'number'); |
| 229 chrome.test.assertTrue('tabId' in details && |
| 230 typeof details.tabId === 'number'); |
| 231 var key = details.tabId + "-" + details.frameId; |
| 232 if (details.type == "main_frame" || details.type == "sub_frame") { |
| 233 tabAndFrameUrls[key] = details.url; |
| 234 } |
| 235 details.frameUrl = tabAndFrameUrls[key] || "unknown frame URL"; |
| 236 } |
| 237 |
| 238 // This assigns unique IDs to frames. The new IDs are only deterministic, if |
| 239 // the frames documents are loaded in order. Don't write browser tests with |
| 240 // more than one frame ID and rely on their numbers. |
| 241 if (!(details.frameId in frameIdMap)) { |
| 242 // Subtract one to discount for {"-1": -1} mapping that always exists. |
| 243 // This gives the first frame the ID 0. |
| 244 frameIdMap[details.frameId] = Object.keys(frameIdMap).length - 1; |
| 245 } |
| 246 details.frameId = frameIdMap[details.frameId]; |
| 247 details.parentFrameId = frameIdMap[details.parentFrameId]; |
| 248 |
| 249 // This assigns unique IDs to newly opened tabs. However, the new IDs are only |
| 250 // deterministic, if the order in which the tabs are opened is deterministic. |
| 251 if (!(details.tabId in tabIdMap)) { |
| 252 // Subtract one because the map is initialized with {"-1": -1}, and the |
| 253 // first tab has ID 0. |
| 254 tabIdMap[details.tabId] = Object.keys(tabIdMap).length - 1; |
| 255 } |
| 256 details.tabId = tabIdMap[details.tabId]; |
| 257 |
| 258 delete details.requestId; |
| 259 delete details.timeStamp; |
| 260 if (details.requestHeaders) { |
| 261 details.requestHeadersValid = checkUserAgent(details.requestHeaders); |
| 262 delete details.requestHeaders; |
| 263 } |
| 264 if (details.responseHeaders) { |
| 265 details.responseHeadersExist = true; |
| 266 delete details.responseHeaders; |
| 267 } |
| 268 |
| 269 // find |details| in expectedEventData |
| 270 var found = false; |
| 271 var label = undefined; |
| 272 expectedEventData.forEach(function (exp) { |
| 273 if (deepEq(exp.event, name) && deepEq(exp.details, details)) { |
| 274 if (found) { |
| 275 chrome.test.fail("Received event twice '" + name + "':" + |
| 276 JSON.stringify(details)); |
| 277 } else { |
| 278 found = true; |
| 279 label = exp.label; |
| 280 } |
| 281 } |
| 282 }); |
| 283 if (!found && !ignoreUnexpected) { |
| 284 console.log("Expected events: " + |
| 285 JSON.stringify(expectedEventData, null, 2)); |
| 286 chrome.test.fail("Received unexpected event '" + name + "':" + |
| 287 JSON.stringify(details, null, 2)); |
| 288 } |
| 289 |
| 290 if (found) { |
| 291 if (logAllRequests) { |
| 292 console.log("Expected: " + name + ": " + JSON.stringify(details)); |
| 293 } |
| 294 capturedEventData.push({label: label, event: name, details: details}); |
| 295 |
| 296 // checkExpecations decrements the counter of pending events. We may only |
| 297 // call it if an expected event has occurred. |
| 298 checkExpectations(); |
| 299 } else { |
| 300 if (logAllRequests) { |
| 301 console.log("NOT Expected: " + name + ": " + JSON.stringify(details)); |
| 302 } |
| 303 capturedUnexpectedData.push({label: label, event: name, details: details}); |
| 304 } |
| 305 |
| 306 if (callback) { |
| 307 window.setTimeout(callback, 0, retval); |
| 308 } else { |
| 309 return retval; |
| 310 } |
| 311 } |
| 312 |
| 313 // Simple array intersection. We use this to filter extraInfoSpec so |
| 314 // that only the allowed specs are sent to each listener. |
| 315 function intersect(array1, array2) { |
| 316 return array1.filter(function(x) { return array2.indexOf(x) != -1; }); |
| 317 } |
| 318 |
| 319 function initListeners(filter, extraInfoSpec) { |
| 320 var onBeforeRequest = function(details) { |
| 321 return captureEvent("onBeforeRequest", details); |
| 322 }; |
| 323 listeners['onBeforeRequest'].push(onBeforeRequest); |
| 324 |
| 325 var onBeforeSendHeaders = function(details) { |
| 326 return captureEvent("onBeforeSendHeaders", details); |
| 327 }; |
| 328 listeners['onBeforeSendHeaders'].push(onBeforeSendHeaders); |
| 329 |
| 330 var onSendHeaders = function(details) { |
| 331 return captureEvent("onSendHeaders", details); |
| 332 }; |
| 333 listeners['onSendHeaders'].push(onSendHeaders); |
| 334 |
| 335 var onHeadersReceived = function(details) { |
| 336 return captureEvent("onHeadersReceived", details); |
| 337 }; |
| 338 listeners['onHeadersReceived'].push(onHeadersReceived); |
| 339 |
| 340 var onAuthRequired = function(details) { |
| 341 return captureEvent("onAuthRequired", details, callback); |
| 342 }; |
| 343 listeners['onAuthRequired'].push(onAuthRequired); |
| 344 |
| 345 var onResponseStarted = function(details) { |
| 346 return captureEvent("onResponseStarted", details); |
| 347 }; |
| 348 listeners['onResponseStarted'].push(onResponseStarted); |
| 349 |
| 350 var onBeforeRedirect = function(details) { |
| 351 return captureEvent("onBeforeRedirect", details); |
| 352 }; |
| 353 listeners['onBeforeRedirect'].push(onBeforeRedirect); |
| 354 |
| 355 var onCompleted = function(details) { |
| 356 return captureEvent("onCompleted", details); |
| 357 }; |
| 358 listeners['onCompleted'].push(onCompleted); |
| 359 |
| 360 var onErrorOccurred = function(details) { |
| 361 return captureEvent("onErrorOccurred", details); |
| 362 }; |
| 363 listeners['onErrorOccurred'].push(onErrorOccurred); |
| 364 |
| 365 chrome.webRequest.onBeforeRequest.addListener( |
| 366 onBeforeRequest, filter, |
| 367 intersect(extraInfoSpec, ["blocking", "requestBody"])); |
| 368 |
| 369 chrome.webRequest.onBeforeSendHeaders.addListener( |
| 370 onBeforeSendHeaders, filter, |
| 371 intersect(extraInfoSpec, ["blocking", "requestHeaders"])); |
| 372 |
| 373 chrome.webRequest.onSendHeaders.addListener( |
| 374 onSendHeaders, filter, |
| 375 intersect(extraInfoSpec, ["requestHeaders"])); |
| 376 |
| 377 chrome.webRequest.onHeadersReceived.addListener( |
| 378 onHeadersReceived, filter, |
| 379 intersect(extraInfoSpec, ["blocking", "responseHeaders"])); |
| 380 |
| 381 chrome.webRequest.onAuthRequired.addListener( |
| 382 onAuthRequired, filter, |
| 383 intersect(extraInfoSpec, ["asyncBlocking", "blocking", |
| 384 "responseHeaders"])); |
| 385 |
| 386 chrome.webRequest.onResponseStarted.addListener( |
| 387 onResponseStarted, filter, |
| 388 intersect(extraInfoSpec, ["responseHeaders"])); |
| 389 |
| 390 chrome.webRequest.onBeforeRedirect.addListener( |
| 391 onBeforeRedirect, filter, intersect(extraInfoSpec, ["responseHeaders"])); |
| 392 |
| 393 chrome.webRequest.onCompleted.addListener( |
| 394 onCompleted, filter, |
| 395 intersect(extraInfoSpec, ["responseHeaders"])); |
| 396 |
| 397 chrome.webRequest.onErrorOccurred.addListener(onErrorOccurred, filter); |
| 398 } |
| 399 |
| 400 function removeListeners() { |
| 401 function helper(eventName) { |
| 402 for (var i in listeners[eventName]) { |
| 403 chrome.webRequest[eventName].removeListener(listeners[eventName][i]); |
| 404 } |
| 405 listeners[eventName].length = 0; |
| 406 chrome.test.assertFalse(chrome.webRequest[eventName].hasListeners()); |
| 407 } |
| 408 helper('onBeforeRequest'); |
| 409 helper('onBeforeSendHeaders'); |
| 410 helper('onAuthRequired'); |
| 411 helper('onSendHeaders'); |
| 412 helper('onHeadersReceived'); |
| 413 helper('onResponseStarted'); |
| 414 helper('onBeforeRedirect'); |
| 415 helper('onCompleted'); |
| 416 helper('onErrorOccurred'); |
| 417 } |
| OLD | NEW |