| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| var getURL = chrome.extension.getURL; |
| var deepEq = chrome.test.checkDeepEq; |
| var expectedEventData; |
| var capturedEventData; |
| var capturedUnexpectedData; |
| var expectedEventOrder; |
| var tabId; |
| var tabIdMap; |
| var frameIdMap; |
| var testServerPort; |
| var testServer = "www.a.com"; |
| var defaultScheme = "http"; |
| var eventsCaptured; |
| var listeners = { |
| 'onBeforeRequest': [], |
| 'onBeforeSendHeaders': [], |
| 'onAuthRequired': [], |
| 'onSendHeaders': [], |
| 'onHeadersReceived': [], |
| 'onResponseStarted': [], |
| 'onBeforeRedirect': [], |
| 'onCompleted': [], |
| 'onErrorOccurred': [] |
| }; |
| |
| // If true, don't bark on events that were not registered via expect(). |
| // These events are recorded in capturedUnexpectedData instead of |
| // capturedEventData. |
| var ignoreUnexpected = false; |
| |
| // This is a debugging aid to print all received events as well as the |
| // information whether they were expected. |
| var logAllRequests = false; |
| |
| function runTests(tests) { |
| var waitForAboutBlank = function(_, info, tab) { |
| if (info.status == "complete" && tab.url == "about:blank") { |
| tabId = tab.id; |
| tabIdMap = {"-1": -1}; |
| tabIdMap[tabId] = 0; |
| chrome.tabs.onUpdated.removeListener(waitForAboutBlank); |
| chrome.test.getConfig(function(config) { |
| testServerPort = config.testServer.port; |
| chrome.test.runTests(tests); |
| }); |
| } |
| }; |
| chrome.tabs.onUpdated.addListener(waitForAboutBlank); |
| chrome.tabs.create({url: "about:blank"}); |
| } |
| |
| // Returns an URL from the test server, fixing up the port. Must be called |
| // from within a test case passed to runTests. |
| function getServerURL(path, opt_host, opt_scheme) { |
| if (!testServerPort) |
| throw new Error("Called getServerURL outside of runTests."); |
| var host = opt_host || testServer; |
| var scheme = opt_scheme || defaultScheme; |
| return scheme + "://" + host + ":" + testServerPort + "/" + path; |
| } |
| |
| // Helper to advance to the next test only when the tab has finished loading. |
| // This is because tabs.update can sometimes fail if the tab is in the middle |
| // of a navigation (from the previous test), resulting in flakiness. |
| function navigateAndWait(url, callback) { |
| var done = chrome.test.listenForever(chrome.tabs.onUpdated, |
| function (_, info, tab) { |
| if (tab.id == tabId && info.status == "complete") { |
| if (callback) callback(); |
| done(); |
| } |
| }); |
| chrome.tabs.update(tabId, {url: url}); |
| } |
| |
| // data: array of extected events, each one is a dictionary: |
| // { label: "<unique identifier>", |
| // event: "<webrequest event type>", |
| // details: { <expected details of the webrequest event> }, |
| // retval: { <dictionary that the event handler shall return> } (optional) |
| // } |
| // order: an array of sequences, e.g. [ ["a", "b", "c"], ["d", "e"] ] means that |
| // event with label "a" needs to occur before event with label "b". The |
| // relative order of "a" and "d" does not matter. |
| // filter: filter dictionary passed on to the event subscription of the |
| // webRequest API. |
| // extraInfoSpec: the union of all desired extraInfoSpecs for the events. |
| function expect(data, order, filter, extraInfoSpec) { |
| expectedEventData = data || []; |
| capturedEventData = []; |
| capturedUnexpectedData = []; |
| expectedEventOrder = order || []; |
| if (expectedEventData.length > 0) { |
| eventsCaptured = chrome.test.callbackAdded(); |
| } |
| tabAndFrameUrls = {}; // Maps "{tabId}-{frameId}" to the URL of the frame. |
| frameIdMap = {"-1": -1}; |
| removeListeners(); |
| resetDeclarativeRules(); |
| initListeners(filter || {urls: ["<all_urls>"]}, extraInfoSpec || []); |
| // Fill in default values. |
| for (var i = 0; i < expectedEventData.length; ++i) { |
| if (!('method' in expectedEventData[i].details)) { |
| expectedEventData[i].details.method = "GET"; |
| } |
| if (!('tabId' in expectedEventData[i].details)) { |
| expectedEventData[i].details.tabId = tabIdMap[tabId]; |
| } |
| if (!('frameId' in expectedEventData[i].details)) { |
| expectedEventData[i].details.frameId = 0; |
| } |
| if (!('parentFrameId' in expectedEventData[i].details)) { |
| expectedEventData[i].details.parentFrameId = -1; |
| } |
| if (!('type' in expectedEventData[i].details)) { |
| expectedEventData[i].details.type = "main_frame"; |
| } |
| } |
| } |
| |
| function checkExpectations() { |
| if (capturedEventData.length < expectedEventData.length) { |
| return; |
| } |
| if (capturedEventData.length > expectedEventData.length) { |
| chrome.test.fail("Recorded too many events. " + |
| JSON.stringify(capturedEventData)); |
| return; |
| } |
| // We have ensured that capturedEventData contains exactly the same elements |
| // as expectedEventData. Now we need to verify the ordering. |
| // Step 1: build positions such that |
| // positions[<event-label>]=<position of this event in capturedEventData> |
| var curPos = 0; |
| var positions = {} |
| capturedEventData.forEach(function (event) { |
| chrome.test.assertTrue(event.hasOwnProperty("label")); |
| positions[event.label] = curPos; |
| curPos++; |
| }); |
| // Step 2: check that elements arrived in correct order |
| expectedEventOrder.forEach(function (order) { |
| var previousLabel = undefined; |
| order.forEach(function(label) { |
| if (previousLabel === undefined) { |
| previousLabel = label; |
| return; |
| } |
| chrome.test.assertTrue(positions[previousLabel] < positions[label], |
| "Event " + previousLabel + " is supposed to arrive before " + |
| label + "."); |
| previousLabel = label; |
| }); |
| }); |
| |
| eventsCaptured(); |
| } |
| |
| // Simple check to see that we have a User-Agent header, and that it contains |
| // an expected value. This is a basic check that the request headers are valid. |
| function checkUserAgent(headers) { |
| for (var i in headers) { |
| if (headers[i].name.toLowerCase() == "user-agent") |
| return headers[i].value.toLowerCase().indexOf("chrome") != -1; |
| } |
| return false; |
| } |
| |
| // Whether the request is missing a tabId and frameId and we're not expecting |
| // a request with the given details. If the method returns true, the event |
| // should be ignored. |
| function isUnexpectedDetachedRequest(name, details) { |
| // This function is responsible for marking detached requests as unexpected. |
| // Non-detached requests are not this function's concern. |
| if (details.tabId !== -1 || details.frameId >= 0) |
| return false; |
| |
| // Only return true if there is no matching expectation for the given details. |
| return !expectedEventData.some(function(exp) { |
| return name === exp.event && |
| exp.details.tabId === -1 && |
| exp.details.frameId === -1 && |
| exp.details.method === details.method && |
| exp.details.url === details.url && |
| exp.details.type === details.type; |
| }); |
| } |
| |
| function captureEvent(name, details, callback) { |
| // frameId should be -1 or positive, but is sometimes -2 (MSG_ROUTING_NONE). |
| // TODO(robwu): This will be resolved once crbug.com/432875 is resolved. |
| if (details.frameId === -2) |
| details.frameId = -1; |
| |
| // Ignore system-level requests like safebrowsing updates and favicon fetches |
| // since they are unpredictable. |
| if (details.type == "other" || |
| isUnexpectedDetachedRequest(name, details) || |
| details.url.match(/\/favicon.ico$/) || |
| details.url.match(/https:\/\/dl.google.com/)) |
| return; |
| |
| // Pull the extra per-event options out of the expected data. These let |
| // us specify special return values per event. |
| var currentIndex = capturedEventData.length; |
| var extraOptions; |
| var retval; |
| if (expectedEventData.length > currentIndex) { |
| retval = |
| expectedEventData[currentIndex].retval_function ? |
| expectedEventData[currentIndex].retval_function(name, details) : |
| expectedEventData[currentIndex].retval; |
| } |
| |
| // Check that the frameId can be used to reliably determine the URL of the |
| // frame that caused requests. |
| if (name == "onBeforeRequest") { |
| chrome.test.assertTrue('frameId' in details && |
| typeof details.frameId === 'number'); |
| chrome.test.assertTrue('tabId' in details && |
| typeof details.tabId === 'number'); |
| var key = details.tabId + "-" + details.frameId; |
| if (details.type == "main_frame" || details.type == "sub_frame") { |
| tabAndFrameUrls[key] = details.url; |
| } |
| details.frameUrl = tabAndFrameUrls[key] || "unknown frame URL"; |
| } |
| |
| // This assigns unique IDs to frames. The new IDs are only deterministic, if |
| // the frames documents are loaded in order. Don't write browser tests with |
| // more than one frame ID and rely on their numbers. |
| if (!(details.frameId in frameIdMap)) { |
| // Subtract one to discount for {"-1": -1} mapping that always exists. |
| // This gives the first frame the ID 0. |
| frameIdMap[details.frameId] = Object.keys(frameIdMap).length - 1; |
| } |
| details.frameId = frameIdMap[details.frameId]; |
| details.parentFrameId = frameIdMap[details.parentFrameId]; |
| |
| // This assigns unique IDs to newly opened tabs. However, the new IDs are only |
| // deterministic, if the order in which the tabs are opened is deterministic. |
| if (!(details.tabId in tabIdMap)) { |
| // Subtract one because the map is initialized with {"-1": -1}, and the |
| // first tab has ID 0. |
| tabIdMap[details.tabId] = Object.keys(tabIdMap).length - 1; |
| } |
| details.tabId = tabIdMap[details.tabId]; |
| |
| delete details.requestId; |
| delete details.timeStamp; |
| if (details.requestHeaders) { |
| details.requestHeadersValid = checkUserAgent(details.requestHeaders); |
| delete details.requestHeaders; |
| } |
| if (details.responseHeaders) { |
| details.responseHeadersExist = true; |
| delete details.responseHeaders; |
| } |
| |
| // find |details| in expectedEventData |
| var found = false; |
| var label = undefined; |
| expectedEventData.forEach(function (exp) { |
| if (deepEq(exp.event, name) && deepEq(exp.details, details)) { |
| if (found) { |
| chrome.test.fail("Received event twice '" + name + "':" + |
| JSON.stringify(details)); |
| } else { |
| found = true; |
| label = exp.label; |
| } |
| } |
| }); |
| if (!found && !ignoreUnexpected) { |
| console.log("Expected events: " + |
| JSON.stringify(expectedEventData, null, 2)); |
| chrome.test.fail("Received unexpected event '" + name + "':" + |
| JSON.stringify(details, null, 2)); |
| } |
| |
| if (found) { |
| if (logAllRequests) { |
| console.log("Expected: " + name + ": " + JSON.stringify(details)); |
| } |
| capturedEventData.push({label: label, event: name, details: details}); |
| |
| // checkExpecations decrements the counter of pending events. We may only |
| // call it if an expected event has occurred. |
| checkExpectations(); |
| } else { |
| if (logAllRequests) { |
| console.log("NOT Expected: " + name + ": " + JSON.stringify(details)); |
| } |
| capturedUnexpectedData.push({label: label, event: name, details: details}); |
| } |
| |
| if (callback) { |
| window.setTimeout(callback, 0, retval); |
| } else { |
| return retval; |
| } |
| } |
| |
| // Simple array intersection. We use this to filter extraInfoSpec so |
| // that only the allowed specs are sent to each listener. |
| function intersect(array1, array2) { |
| return array1.filter(function(x) { return array2.indexOf(x) != -1; }); |
| } |
| |
| function initListeners(filter, extraInfoSpec) { |
| var onBeforeRequest = function(details) { |
| return captureEvent("onBeforeRequest", details); |
| }; |
| listeners['onBeforeRequest'].push(onBeforeRequest); |
| |
| var onBeforeSendHeaders = function(details) { |
| return captureEvent("onBeforeSendHeaders", details); |
| }; |
| listeners['onBeforeSendHeaders'].push(onBeforeSendHeaders); |
| |
| var onSendHeaders = function(details) { |
| return captureEvent("onSendHeaders", details); |
| }; |
| listeners['onSendHeaders'].push(onSendHeaders); |
| |
| var onHeadersReceived = function(details) { |
| return captureEvent("onHeadersReceived", details); |
| }; |
| listeners['onHeadersReceived'].push(onHeadersReceived); |
| |
| var onAuthRequired = function(details) { |
| return captureEvent("onAuthRequired", details, callback); |
| }; |
| listeners['onAuthRequired'].push(onAuthRequired); |
| |
| var onResponseStarted = function(details) { |
| return captureEvent("onResponseStarted", details); |
| }; |
| listeners['onResponseStarted'].push(onResponseStarted); |
| |
| var onBeforeRedirect = function(details) { |
| return captureEvent("onBeforeRedirect", details); |
| }; |
| listeners['onBeforeRedirect'].push(onBeforeRedirect); |
| |
| var onCompleted = function(details) { |
| return captureEvent("onCompleted", details); |
| }; |
| listeners['onCompleted'].push(onCompleted); |
| |
| var onErrorOccurred = function(details) { |
| return captureEvent("onErrorOccurred", details); |
| }; |
| listeners['onErrorOccurred'].push(onErrorOccurred); |
| |
| chrome.webRequest.onBeforeRequest.addListener( |
| onBeforeRequest, filter, |
| intersect(extraInfoSpec, ["blocking", "requestBody"])); |
| |
| chrome.webRequest.onBeforeSendHeaders.addListener( |
| onBeforeSendHeaders, filter, |
| intersect(extraInfoSpec, ["blocking", "requestHeaders"])); |
| |
| chrome.webRequest.onSendHeaders.addListener( |
| onSendHeaders, filter, |
| intersect(extraInfoSpec, ["requestHeaders"])); |
| |
| chrome.webRequest.onHeadersReceived.addListener( |
| onHeadersReceived, filter, |
| intersect(extraInfoSpec, ["blocking", "responseHeaders"])); |
| |
| chrome.webRequest.onAuthRequired.addListener( |
| onAuthRequired, filter, |
| intersect(extraInfoSpec, ["asyncBlocking", "blocking", |
| "responseHeaders"])); |
| |
| chrome.webRequest.onResponseStarted.addListener( |
| onResponseStarted, filter, |
| intersect(extraInfoSpec, ["responseHeaders"])); |
| |
| chrome.webRequest.onBeforeRedirect.addListener( |
| onBeforeRedirect, filter, intersect(extraInfoSpec, ["responseHeaders"])); |
| |
| chrome.webRequest.onCompleted.addListener( |
| onCompleted, filter, |
| intersect(extraInfoSpec, ["responseHeaders"])); |
| |
| chrome.webRequest.onErrorOccurred.addListener(onErrorOccurred, filter); |
| } |
| |
| function removeListeners() { |
| function helper(eventName) { |
| for (var i in listeners[eventName]) { |
| chrome.webRequest[eventName].removeListener(listeners[eventName][i]); |
| } |
| listeners[eventName].length = 0; |
| chrome.test.assertFalse(chrome.webRequest[eventName].hasListeners()); |
| } |
| helper('onBeforeRequest'); |
| helper('onBeforeSendHeaders'); |
| helper('onAuthRequired'); |
| helper('onSendHeaders'); |
| helper('onHeadersReceived'); |
| helper('onResponseStarted'); |
| helper('onBeforeRedirect'); |
| helper('onCompleted'); |
| helper('onErrorOccurred'); |
| } |
| |
| function resetDeclarativeRules() { |
| chrome.declarativeWebRequest.onRequest.removeRules(); |
| } |