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