mockpushserviceparent.js (5248B)
1 /* eslint-env mozilla/chrome-script */ 2 3 "use strict"; 4 5 /** 6 * Defers one or more callbacks until the next turn of the event loop. Multiple 7 * callbacks are executed in order. 8 * 9 * @param {Function[]} callbacks The callbacks to execute. One callback will be 10 * executed per tick. 11 */ 12 function waterfall(...callbacks) { 13 callbacks 14 .reduce( 15 (promise, callback) => 16 promise.then(() => { 17 callback(); 18 }), 19 Promise.resolve() 20 ) 21 .catch(console.error); 22 } 23 24 /** 25 * Minimal implementation of a mock WebSocket connect to be used with 26 * PushService. Forwards and receive messages from the implementation 27 * that lives in the content process. 28 */ 29 function MockWebSocketParent(originalURI) { 30 this._originalURI = originalURI; 31 } 32 33 MockWebSocketParent.prototype = { 34 _originalURI: null, 35 36 _listener: null, 37 _context: null, 38 39 QueryInterface: ChromeUtils.generateQI(["nsIWebSocketChannel"]), 40 41 get originalURI() { 42 return this._originalURI; 43 }, 44 45 asyncOpen(uri, origin, originAttributes, windowId, listener, context) { 46 this._listener = listener; 47 this._context = context; 48 waterfall(() => this._listener.onStart(this._context)); 49 }, 50 51 sendMsg(msg) { 52 sendAsyncMessage("socket-client-msg", msg); 53 }, 54 55 close() { 56 waterfall(() => this._listener.onStop(this._context, Cr.NS_OK)); 57 }, 58 59 serverSendMsg(msg) { 60 waterfall( 61 () => this._listener.onMessageAvailable(this._context, msg), 62 () => this._listener.onAcknowledge(this._context, 0) 63 ); 64 }, 65 }; 66 67 var pushService = Cc["@mozilla.org/push/Service;1"].getService( 68 Ci.nsIPushService 69 ).wrappedJSObject; 70 71 var mockSocket; 72 var serverMsgs = []; 73 74 addMessageListener("socket-setup", function () { 75 pushService.replaceServiceBackend({ 76 serverURI: "wss://push.example.org/", 77 makeWebSocket(uri) { 78 mockSocket = new MockWebSocketParent(uri); 79 while (serverMsgs.length) { 80 let msg = serverMsgs.shift(); 81 mockSocket.serverSendMsg(msg); 82 } 83 return mockSocket; 84 }, 85 }); 86 }); 87 88 addMessageListener("socket-teardown", function () { 89 pushService 90 .restoreServiceBackend() 91 .then(_ => { 92 serverMsgs.length = 0; 93 if (mockSocket) { 94 mockSocket.close(); 95 mockSocket = null; 96 } 97 sendAsyncMessage("socket-server-teardown"); 98 }) 99 .catch(error => { 100 console.error(`Error restoring service backend: ${error}`); 101 }); 102 }); 103 104 addMessageListener("socket-server-msg", function (msg) { 105 if (mockSocket) { 106 mockSocket.serverSendMsg(msg); 107 } else { 108 serverMsgs.push(msg); 109 } 110 }); 111 112 var MockService = { 113 requestID: 1, 114 resolvers: new Map(), 115 116 sendRequest(name, params) { 117 return new Promise((resolve, reject) => { 118 let id = this.requestID++; 119 this.resolvers.set(id, { resolve, reject }); 120 sendAsyncMessage("service-request", { 121 name, 122 id, 123 // The request params from the real push service may contain a 124 // principal, which cannot be passed to the unprivileged 125 // mochitest scope, and will cause the message to be dropped if 126 // present. The mochitest scope fortunately does not need the 127 // principal, though, so set it to null before sending. 128 params: Object.assign({}, params, { principal: null }), 129 }); 130 }); 131 }, 132 133 handleResponse(response) { 134 if (!this.resolvers.has(response.id)) { 135 console.error(`Unexpected response for request ${response.id}`); 136 return; 137 } 138 let resolver = this.resolvers.get(response.id); 139 this.resolvers.delete(response.id); 140 if (response.error) { 141 resolver.reject(response.error); 142 } else { 143 resolver.resolve(response.result); 144 } 145 }, 146 147 init() {}, 148 149 register(pageRecord) { 150 return this.sendRequest("register", pageRecord); 151 }, 152 153 registration(pageRecord) { 154 return this.sendRequest("registration", pageRecord); 155 }, 156 157 unregister(pageRecord) { 158 return this.sendRequest("unregister", pageRecord); 159 }, 160 161 reportDeliveryError(messageId, reason) { 162 sendAsyncMessage("service-delivery-error", { 163 messageId, 164 reason, 165 }); 166 }, 167 168 uninit() { 169 return Promise.resolve(); 170 }, 171 }; 172 173 async function replaceService(service) { 174 // `?.` because `service` can be null 175 // (either by calling this function with null, or the push module doesn't have the 176 // field at all e.g. in GeckoView) 177 // Passing null here resets it to the default implementation on desktop 178 // (so `.service` never becomes null there) but not for GeckoView. 179 // XXX(krosylight): we need to remove this deviation. 180 await pushService.service?.uninit(); 181 pushService.service = service; 182 await pushService.service?.init(); 183 } 184 185 addMessageListener("service-replace", function () { 186 replaceService(MockService) 187 .then(_ => { 188 sendAsyncMessage("service-replaced"); 189 }) 190 .catch(error => { 191 console.error(`Error replacing service: ${error}`); 192 }); 193 }); 194 195 addMessageListener("service-restore", function () { 196 replaceService(null) 197 .then(_ => { 198 sendAsyncMessage("service-restored"); 199 }) 200 .catch(error => { 201 console.error(`Error restoring service: ${error}`); 202 }); 203 }); 204 205 addMessageListener("service-response", function (response) { 206 MockService.handleResponse(response); 207 });