tor-browser

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

test_utils.js (8472B)


      1 "use strict";
      2 
      3 const url = SimpleTest.getTestFileURL("mockpushserviceparent.js");
      4 const chromeScript = SpecialPowers.loadChromeScript(url);
      5 
      6 /**
      7 * Replaces `PushService.sys.mjs` with a mock implementation that handles requests
      8 * from the DOM API. This allows tests to simulate local errors and error
      9 * reporting, bypassing the `PushService.sys.mjs` machinery.
     10 */
     11 async function replacePushService(mockService) {
     12  chromeScript.addMessageListener("service-delivery-error", function (msg) {
     13    mockService.reportDeliveryError(msg.messageId, msg.reason);
     14  });
     15  chromeScript.addMessageListener("service-request", function (msg) {
     16    let promise;
     17    try {
     18      let handler = mockService[msg.name];
     19      promise = Promise.resolve(handler(msg.params));
     20    } catch (error) {
     21      promise = Promise.reject(error);
     22    }
     23    promise.then(
     24      result => {
     25        chromeScript.sendAsyncMessage("service-response", {
     26          id: msg.id,
     27          result,
     28        });
     29      },
     30      error => {
     31        chromeScript.sendAsyncMessage("service-response", {
     32          id: msg.id,
     33          error,
     34        });
     35      }
     36    );
     37  });
     38  await new Promise(resolve => {
     39    chromeScript.addMessageListener("service-replaced", function onReplaced() {
     40      chromeScript.removeMessageListener("service-replaced", onReplaced);
     41      resolve();
     42    });
     43    chromeScript.sendAsyncMessage("service-replace");
     44  });
     45 }
     46 
     47 async function restorePushService() {
     48  await new Promise(resolve => {
     49    chromeScript.addMessageListener("service-restored", function onRestored() {
     50      chromeScript.removeMessageListener("service-restored", onRestored);
     51      resolve();
     52    });
     53    chromeScript.sendAsyncMessage("service-restore");
     54  });
     55 }
     56 
     57 let currentMockSocket = null;
     58 
     59 /**
     60 * Sets up a mock connection for the WebSocket backend. This only replaces
     61 * the transport layer; `PushService.sys.mjs` still handles DOM API requests,
     62 * observes permission changes, writes to IndexedDB, and notifies service
     63 * workers of incoming push messages.
     64 */
     65 function setupMockPushSocket(mockWebSocket) {
     66  currentMockSocket = mockWebSocket;
     67  currentMockSocket._isActive = true;
     68  chromeScript.sendAsyncMessage("socket-setup");
     69  chromeScript.addMessageListener("socket-client-msg", function (msg) {
     70    mockWebSocket.handleMessage(msg);
     71  });
     72 }
     73 
     74 function teardownMockPushSocket() {
     75  if (currentMockSocket) {
     76    return new Promise(resolve => {
     77      currentMockSocket._isActive = false;
     78      chromeScript.addMessageListener("socket-server-teardown", resolve);
     79      chromeScript.sendAsyncMessage("socket-teardown");
     80    });
     81  }
     82  return Promise.resolve();
     83 }
     84 
     85 /**
     86 * Minimal implementation of web sockets for use in testing. Forwards
     87 * messages to a mock web socket in the parent process that is used
     88 * by the push service.
     89 */
     90 class MockWebSocket {
     91  // Default implementation to make the push server work minimally.
     92  // Override methods to implement custom functionality.
     93  constructor() {
     94    this.userAgentID = "8e1c93a9-139b-419c-b200-e715bb1e8ce8";
     95    this.registerCount = 0;
     96    // We only allow one active mock web socket to talk to the parent.
     97    // This flag is used to keep track of which mock web socket is active.
     98    this._isActive = false;
     99  }
    100 
    101  onHello() {
    102    this.serverSendMsg(
    103      JSON.stringify({
    104        messageType: "hello",
    105        uaid: this.userAgentID,
    106        status: 200,
    107        use_webpush: true,
    108      })
    109    );
    110  }
    111 
    112  onRegister(request) {
    113    this.serverSendMsg(
    114      JSON.stringify({
    115        messageType: "register",
    116        uaid: this.userAgentID,
    117        channelID: request.channelID,
    118        status: 200,
    119        pushEndpoint: "https://example.com/endpoint/" + this.registerCount++,
    120      })
    121    );
    122  }
    123 
    124  onUnregister(request) {
    125    this.serverSendMsg(
    126      JSON.stringify({
    127        messageType: "unregister",
    128        channelID: request.channelID,
    129        status: 200,
    130      })
    131    );
    132  }
    133 
    134  onAck() {
    135    // Do nothing.
    136  }
    137 
    138  handleMessage(msg) {
    139    let request = JSON.parse(msg);
    140    let messageType = request.messageType;
    141    switch (messageType) {
    142      case "hello":
    143        this.onHello(request);
    144        break;
    145      case "register":
    146        this.onRegister(request);
    147        break;
    148      case "unregister":
    149        this.onUnregister(request);
    150        break;
    151      case "ack":
    152        this.onAck(request);
    153        break;
    154      default:
    155        throw new Error("Unexpected message: " + messageType);
    156    }
    157  }
    158 
    159  serverSendMsg(msg) {
    160    if (this._isActive) {
    161      chromeScript.sendAsyncMessage("socket-server-msg", msg);
    162    }
    163  }
    164 }
    165 
    166 // Remove permissions and prefs when the test finishes.
    167 SimpleTest.registerCleanupFunction(async function () {
    168  await new Promise(resolve => SpecialPowers.flushPermissions(resolve));
    169  await SpecialPowers.flushPrefEnv();
    170  await restorePushService();
    171  await teardownMockPushSocket();
    172 });
    173 
    174 function setPushPermission(allow) {
    175  let permissions = [
    176    { type: "desktop-notification", allow, context: document },
    177  ];
    178 
    179  if (isXOrigin) {
    180    // We need to add permission for the xorigin tests. In xorigin tests, the
    181    // test page will be run under third-party context, so we need to use
    182    // partitioned principal to add the permission.
    183    let partitionedPrincipal =
    184      SpecialPowers.wrap(document).partitionedPrincipal;
    185 
    186    permissions.push({
    187      type: "desktop-notification",
    188      allow,
    189      context: {
    190        url: partitionedPrincipal.originNoSuffix,
    191        originAttributes: {
    192          partitionKey: partitionedPrincipal.originAttributes.partitionKey,
    193        },
    194      },
    195    });
    196  }
    197 
    198  return SpecialPowers.pushPermissions(permissions);
    199 }
    200 
    201 function setupPrefs() {
    202  return SpecialPowers.pushPrefEnv({
    203    set: [
    204      ["dom.push.enabled", true],
    205      ["dom.push.connection.enabled", true],
    206      ["dom.push.maxRecentMessageIDsPerSubscription", 0],
    207      ["dom.serviceWorkers.exemptFromPerDomainMax", true],
    208      ["dom.serviceWorkers.enabled", true],
    209      ["dom.serviceWorkers.testing.enabled", true],
    210    ],
    211  });
    212 }
    213 
    214 async function setupPrefsAndReplaceService(mockService) {
    215  await replacePushService(mockService);
    216  await setupPrefs();
    217 }
    218 
    219 function setupPrefsAndMockSocket(mockSocket) {
    220  setupMockPushSocket(mockSocket);
    221  return setupPrefs();
    222 }
    223 
    224 function injectControlledFrame(target = document.body) {
    225  return new Promise(function (res) {
    226    var iframe = document.createElement("iframe");
    227    iframe.src = "/tests/dom/push/test/frame.html";
    228 
    229    var controlledFrame = {
    230      remove() {
    231        target.removeChild(iframe);
    232        iframe = null;
    233      },
    234      waitOnWorkerMessage(type) {
    235        return iframe
    236          ? iframe.contentWindow.waitOnWorkerMessage(type)
    237          : Promise.reject(new Error("Frame removed from document"));
    238      },
    239      innerWindowId() {
    240        return SpecialPowers.wrap(iframe).browsingContext.currentWindowContext
    241          .innerWindowId;
    242      },
    243    };
    244 
    245    iframe.onload = () => res(controlledFrame);
    246    target.appendChild(iframe);
    247  });
    248 }
    249 
    250 function sendRequestToWorker(request) {
    251  return navigator.serviceWorker.ready.then(registration => {
    252    return new Promise((resolve, reject) => {
    253      var channel = new MessageChannel();
    254      channel.port1.onmessage = e => {
    255        (e.data.error ? reject : resolve)(e.data);
    256      };
    257      registration.active.postMessage(request, [channel.port2]);
    258    });
    259  });
    260 }
    261 
    262 function waitForActive(swr) {
    263  let sw = swr.installing || swr.waiting || swr.active;
    264  return new Promise(resolve => {
    265    if (sw.state === "activated") {
    266      resolve(swr);
    267      return;
    268    }
    269    sw.addEventListener("statechange", function onStateChange() {
    270      if (sw.state === "activated") {
    271        sw.removeEventListener("statechange", onStateChange);
    272        resolve(swr);
    273      }
    274    });
    275  });
    276 }
    277 
    278 function base64UrlDecode(s) {
    279  s = s.replace(/-/g, "+").replace(/_/g, "/");
    280 
    281  // Replace padding if it was stripped by the sender.
    282  // See http://tools.ietf.org/html/rfc4648#section-4
    283  switch (s.length % 4) {
    284    case 0:
    285      break; // No pad chars in this case
    286    case 2:
    287      s += "==";
    288      break; // Two pad chars
    289    case 3:
    290      s += "=";
    291      break; // One pad char
    292    default:
    293      throw new Error("Illegal base64url string!");
    294  }
    295 
    296  // With correct padding restored, apply the standard base64 decoder
    297  var decoded = atob(s);
    298 
    299  var array = new Uint8Array(new ArrayBuffer(decoded.length));
    300  for (var i = 0; i < decoded.length; i++) {
    301    array[i] = decoded.charCodeAt(i);
    302  }
    303  return array;
    304 }