Bug 1852754: part 9) Add tests for dynamically loading <link rel="prefetch"> elements...
[gecko.git] / services / automation / ServicesAutomation.sys.mjs
blobcaad63d452b8dc1d85f1bfe268de0a13739ef71b
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /*
6  * This module is used in automation to connect the browser to
7  * a specific FxA account and trigger FX Sync.
8  *
9  * To use it, you can call this sequence:
10  *
11  *    initConfig("https://accounts.stage.mozaws.net");
12  *    await Authentication.signIn(username, password);
13  *    await Sync.triggerSync();
14  *    await Authentication.signOut();
15  *
16  *
17  * Where username is your FxA e-mail. it will connect your browser
18  * to that account and trigger a Sync (on stage servers.)
19  *
20  * You can also use the convenience function that does everything:
21  *
22  *    await triggerSync(username, password, "https://accounts.stage.mozaws.net");
23  *
24  */
26 const lazy = {};
28 ChromeUtils.defineESModuleGetters(lazy, {
29   FxAccountsClient: "resource://gre/modules/FxAccountsClient.sys.mjs",
30   FxAccountsConfig: "resource://gre/modules/FxAccountsConfig.sys.mjs",
31   Log: "resource://gre/modules/Log.sys.mjs",
32   Svc: "resource://services-sync/util.sys.mjs",
33   Weave: "resource://services-sync/main.sys.mjs",
34   clearTimeout: "resource://gre/modules/Timer.sys.mjs",
35   setTimeout: "resource://gre/modules/Timer.sys.mjs",
36 });
38 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
39   return ChromeUtils.importESModule(
40     "resource://gre/modules/FxAccounts.sys.mjs"
41   ).getFxAccountsSingleton();
42 });
44 const AUTOCONFIG_PREF = "identity.fxaccounts.autoconfig.uri";
47  * Log helpers.
48  */
49 var _LOG = [];
51 function LOG(msg, error) {
52   console.debug(msg);
53   _LOG.push(msg);
54   if (error) {
55     console.debug(JSON.stringify(error));
56     _LOG.push(JSON.stringify(error));
57   }
60 function dumpLogs() {
61   let res = _LOG.join("\n");
62   _LOG = [];
63   return res;
66 function promiseObserver(aEventName) {
67   LOG("wait for " + aEventName);
68   return new Promise(resolve => {
69     let handler = () => {
70       lazy.Svc.Obs.remove(aEventName, handler);
71       resolve();
72     };
73     let handlerTimeout = () => {
74       lazy.Svc.Obs.remove(aEventName, handler);
75       LOG("handler timed out " + aEventName);
76       resolve();
77     };
78     lazy.Svc.Obs.add(aEventName, handler);
79     lazy.setTimeout(handlerTimeout, 3000);
80   });
84  *  Authentication
85  *
86  *  Used to sign in an FxA account, takes care of
87  *  the e-mail verification flow.
88  *
89  *  Usage:
90  *
91  *    await Authentication.signIn(username, password);
92  */
93 export var Authentication = {
94   async isLoggedIn() {
95     return !!(await this.getSignedInUser());
96   },
98   async isReady() {
99     let user = await this.getSignedInUser();
100     if (user) {
101       LOG("current user " + JSON.stringify(user));
102     }
103     return user && user.verified;
104   },
106   async getSignedInUser() {
107     try {
108       return await lazy.fxAccounts.getSignedInUser();
109     } catch (error) {
110       LOG("getSignedInUser() failed", error);
111       throw error;
112     }
113   },
115   async shortWaitForVerification(ms) {
116     LOG("shortWaitForVerification");
117     let userData = await this.getSignedInUser();
118     let timeoutID;
119     LOG("set a timeout");
120     let timeoutPromise = new Promise(resolve => {
121       timeoutID = lazy.setTimeout(() => {
122         LOG(`Warning: no verification after ${ms}ms.`);
123         resolve();
124       }, ms);
125     });
126     LOG("set a fxAccounts.whenVerified");
127     await Promise.race([
128       lazy.fxAccounts
129         .whenVerified(userData)
130         .finally(() => lazy.clearTimeout(timeoutID)),
131       timeoutPromise,
132     ]);
133     LOG("done");
134     return this.isReady();
135   },
137   async _confirmUser(uri) {
138     LOG("Open new tab and load verification page");
139     let mainWindow = Services.wm.getMostRecentWindow("navigator:browser");
140     let newtab = mainWindow.gBrowser.addWebTab(uri);
141     let win = mainWindow.gBrowser.getBrowserForTab(newtab);
142     win.addEventListener("load", function (e) {
143       LOG("load");
144     });
146     win.addEventListener("loadstart", function (e) {
147       LOG("loadstart");
148     });
150     win.addEventListener("error", function (msg, url, lineNo, columnNo, error) {
151       var string = msg.toLowerCase();
152       var substring = "script error";
153       if (string.indexOf(substring) > -1) {
154         LOG("Script Error: See Browser Console for Detail");
155       } else {
156         var message = [
157           "Message: " + msg,
158           "URL: " + url,
159           "Line: " + lineNo,
160           "Column: " + columnNo,
161           "Error object: " + JSON.stringify(error),
162         ].join(" - ");
164         LOG(message);
165       }
166     });
168     LOG("wait for page to load");
169     await new Promise(resolve => {
170       let handlerTimeout = () => {
171         LOG("timed out ");
172         resolve();
173       };
174       var timer = lazy.setTimeout(handlerTimeout, 10000);
175       win.addEventListener("loadend", function () {
176         resolve();
177         lazy.clearTimeout(timer);
178       });
179     });
180     LOG("Page Loaded");
181     let didVerify = await this.shortWaitForVerification(10000);
182     LOG("remove tab");
183     mainWindow.gBrowser.removeTab(newtab);
184     return didVerify;
185   },
187   /*
188    * This whole verification process may be bypassed if the
189    * account is allow-listed.
190    */
191   async _completeVerification(username) {
192     LOG("Fetching mail (from restmail) for user " + username);
193     let restmailURI = `https://www.restmail.net/mail/${encodeURIComponent(
194       username
195     )}`;
196     let triedAlready = new Set();
197     const tries = 10;
198     const normalWait = 4000;
199     for (let i = 0; i < tries; ++i) {
200       let resp = await fetch(restmailURI);
201       let messages = await resp.json();
202       // Sort so that the most recent emails are first.
203       messages.sort((a, b) => new Date(b.receivedAt) - new Date(a.receivedAt));
204       for (let m of messages) {
205         // We look for a link that has a x-link that we haven't yet tried.
206         if (!m.headers["x-link"] || triedAlready.has(m.headers["x-link"])) {
207           continue;
208         }
209         if (!m.headers["x-verify-code"]) {
210           continue;
211         }
212         let confirmLink = m.headers["x-link"];
213         triedAlready.add(confirmLink);
214         LOG("Trying confirmation link " + confirmLink);
215         try {
216           if (await this._confirmUser(confirmLink)) {
217             LOG("confirmation done");
218             return true;
219           }
220           LOG("confirmation failed");
221         } catch (e) {
222           LOG(
223             "Warning: Failed to follow confirmation link: " +
224               lazy.Log.exceptionStr(e)
225           );
226         }
227       }
228       if (i === 0) {
229         // first time through after failing we'll do this.
230         LOG("resendVerificationEmail");
231         await lazy.fxAccounts.resendVerificationEmail();
232       }
233       if (await this.shortWaitForVerification(normalWait)) {
234         return true;
235       }
236     }
237     // One last try.
238     return this.shortWaitForVerification(normalWait);
239   },
241   async signIn(username, password) {
242     LOG("Login user: " + username);
243     try {
244       // Required here since we don't go through the real login page
245       LOG("Calling FxAccountsConfig.ensureConfigured");
246       await lazy.FxAccountsConfig.ensureConfigured();
247       let client = new lazy.FxAccountsClient();
248       LOG("Signing in");
249       let credentials = await client.signIn(username, password, true);
250       LOG("Signed in, setting up the signed user in fxAccounts");
251       await lazy.fxAccounts._internal.setSignedInUser(credentials);
253       // If the account is not allow-listed for tests, we need to verify it
254       if (!credentials.verified) {
255         LOG("We need to verify the account");
256         await this._completeVerification(username);
257       } else {
258         LOG("Credentials already verified");
259       }
260       return true;
261     } catch (error) {
262       LOG("signIn() failed", error);
263       throw error;
264     }
265   },
267   async signOut() {
268     if (await Authentication.isLoggedIn()) {
269       // Note: This will clean up the device ID.
270       await lazy.fxAccounts.signOut();
271     }
272   },
276  * Sync
278  * Used to trigger sync.
280  * usage:
282  *   await Sync.triggerSync();
283  */
284 export var Sync = {
285   getSyncLogsDirectory() {
286     return PathUtils.join(PathUtils.profileDir, "weave", "logs");
287   },
289   async init() {
290     lazy.Svc.Obs.add("weave:service:sync:error", this);
291     lazy.Svc.Obs.add("weave:service:setup-complete", this);
292     lazy.Svc.Obs.add("weave:service:tracking-started", this);
293     // Delay the automatic sync operations, so we can trigger it manually
294     lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.immediateInterval", 7200);
295     lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.idleInterval", 7200);
296     lazy.Weave.Svc.PrefBranch.setIntPref("scheduler.activeInterval", 7200);
297     lazy.Weave.Svc.PrefBranch.setIntPref("syncThreshold", 10000000);
298     // Wipe all the logs
299     await this.wipeLogs();
300   },
302   observe(subject, topic, data) {
303     LOG("Event received " + topic);
304   },
306   async configureSync() {
307     // todo, enable all sync engines here
308     // the addon engine requires kinto creds...
309     LOG("configuring sync");
310     console.assert(await Authentication.isReady(), "You are not connected");
311     await lazy.Weave.Service.configure();
312     if (!lazy.Weave.Status.ready) {
313       await promiseObserver("weave:service:ready");
314     }
315     if (lazy.Weave.Service.locked) {
316       await promiseObserver("weave:service:resyncs-finished");
317     }
318   },
320   /*
321    * triggerSync() runs the whole process of Syncing.
322    *
323    * returns 1 on success, 0 on failure.
324    */
325   async triggerSync() {
326     if (!(await Authentication.isLoggedIn())) {
327       LOG("Not connected");
328       return 1;
329     }
330     await this.init();
331     let result = 1;
332     try {
333       await this.configureSync();
334       LOG("Triggering a sync");
335       await lazy.Weave.Service.sync();
337       // wait a second for things to settle...
338       await new Promise(resolve => lazy.setTimeout(resolve, 1000));
340       LOG("Sync done");
341       result = 0;
342     } catch (error) {
343       LOG("triggerSync() failed", error);
344     }
346     return result;
347   },
349   async wipeLogs() {
350     let outputDirectory = this.getSyncLogsDirectory();
351     if (!(await IOUtils.exists(outputDirectory))) {
352       return;
353     }
354     LOG("Wiping existing Sync logs");
355     try {
356       await IOUtils.remove(outputDirectory, { recursive: true });
357     } catch (error) {
358       LOG("wipeLogs() failed", error);
359     }
360   },
362   async getLogs() {
363     let outputDirectory = this.getSyncLogsDirectory();
364     let entries = [];
366     if (await IOUtils.exists(outputDirectory)) {
367       // Iterate through the directory
368       for (const path of await IOUtils.getChildren(outputDirectory)) {
369         const info = await IOUtils.stat(path);
371         entries.push({
372           path,
373           name: PathUtils.filename(path),
374           lastModified: info.lastModified,
375         });
376       }
378       entries.sort(function (a, b) {
379         return b.lastModified - a.lastModified;
380       });
381     }
383     const promises = entries.map(async entry => {
384       const content = await IOUtils.readUTF8(entry.path);
385       return {
386         name: entry.name,
387         content,
388       };
389     });
390     return Promise.all(promises);
391   },
394 export function initConfig(autoconfig) {
395   Services.prefs.setCharPref(AUTOCONFIG_PREF, autoconfig);
398 export async function triggerSync(username, password, autoconfig) {
399   initConfig(autoconfig);
400   await Authentication.signIn(username, password);
401   var result = await Sync.triggerSync();
402   await Authentication.signOut();
403   var logs = {
404     sync: await Sync.getLogs(),
405     condprof: [
406       {
407         name: "console.txt",
408         content: dumpLogs(),
409       },
410     ],
411   };
412   return {
413     result,
414     logs,
415   };