Bug 1548404 - Update UITour to reflect the decoupling of FxA and Sync. r=MattN,andreio,rfkelly

Differential Revision: https://phabricator.services.mozilla.com/D51976

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Mark Hammond 2019-11-12 23:58:03 +00:00
Родитель f9f5d6809a
Коммит ab41353fa5
6 изменённых файлов: 440 добавлений и 45 удалений

Просмотреть файл

@ -387,6 +387,8 @@ if (typeof Mozilla == "undefined") {
* <li>{@link Mozilla.UITour.Configuration.Search|selectedSearchEngine}
* - DEPRECATED, use 'search'</li>
* <li>{@link Mozilla.UITour.Configuration.Sync|sync}</li>
* - DEPRECATED, use 'fxa'</li>
* <li>{@link Mozilla.UITour.Configuration.FxA|fxa}</li>
* </ul>
*/
@ -448,6 +450,65 @@ if (typeof Mozilla == "undefined") {
* @since 50
*/
/**
* FxA status, including whether FxA is connected, device counts, services
* connected to this browser and services externally connected to the account.
* @typedef {Object} Mozilla.UITour.Configuration.FxA
* @property {Boolean} setup - Whether FxA is setup on this device. If false,
* no other properties will exist.
* @property {Number} [numOtherDevices] - Number of devices connected to this
* account, not counting this device.
* @property {Object.<String, Number>} [numDevicesByType] - A count of devices
* connected to the account by device 'type'. Valid values for type are
* defined by the FxA server but roughly correspond to form-factor with
* values like 'desktop', 'mobile', 'vr', etc.
* @property {Mozilla.UITour.Configuration.AccountServices} [accountServices] -
* Information about services attached to this account. These services
* may be enabled on devices or applications external to this
* browser and should not be confused with devices. For example, if the user
* has enabled Monitor or Lockwise on one or more devices - including on
* this device - that service will have a single entry here.
* @property {Mozilla.UITour.Configuration.BrowserServices} [browserServices] -
* Information about account services attached to this browser, and with
* special support implemented by this browser. You should not expect
* every accountService connected in this browser to get a special entry
* here. Indeed, what services, and in what circumstances they may appear
* here in the future is largely TBD.
* @since 71
*/
/**
* Information about clients attached to the account.
* An object. The key is a string ID of the attached service. A list of attached
* service IDs can be found at
* {@link https://docs.telemetry.mozilla.org/datasets/fxa_metrics/attribution.html#service-attribution|
* on our telemetry documentation site}
* The value is a {@link Mozilla.UITour.Configuration.AccountService}
* @typedef {Object.<string, Mozilla.UITour.Configuration.AccountService>} Mozilla.UITour.Configuration.AccountService
* @since 71
*/
/**
* Information about an account service
* @typedef {Object} Mozilla.UITour.Configuration.AccountService
* @property {String} id - The service ID. A list of attached
* service IDs can be found at
* {@link https://docs.telemetry.mozilla.org/datasets/fxa_metrics/attribution.html#service-attribution|
* on our telemetry documentation site}
* @property {Number} lastAccessedWeeksAgo - How many weeks ago the service
* was accessed by this account.
* @since 71
*/
/**
* Information about a services attached to the browser. All properties are
* optional and only exist if the service is enabled.
*
* @typedef {Object} Mozilla.UITour.Configuration.BrowserServices
* @property {Mozilla.UITour.Configuration.Sync} sync - If sync is configured
* @since 71
*/
/**
* Array of UI {@link Mozilla.UITour.Target|Targets} currently available to be annotated.
* @typedef {Mozilla.UITour.Target[]} Mozilla.UITour.Configuration.AvailableTargets

Просмотреть файл

@ -24,6 +24,11 @@ ChromeUtils.defineModuleGetter(
"CustomizableUI",
"resource:///modules/CustomizableUI.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"fxAccounts",
"resource://gre/modules/FxAccounts.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"FxAccounts",
@ -1669,6 +1674,13 @@ var UITour = {
});
});
break;
case "fxa":
this.getFxA(aMessageManager, aCallbackID);
break;
// NOTE: 'sync' is deprecated and should be removed in Firefox 73 (because
// by then, all consumers will have upgraded to use 'fxa' in that version
// and later.)
case "sync":
this.sendPageCallback(aMessageManager, aCallbackID, {
setup: Services.prefs.prefHasUserValue("services.sync.username"),
@ -1721,6 +1733,77 @@ var UITour = {
}
},
getFxA(aMessageManager, aCallbackID) {
(async () => {
let setup = !!(await fxAccounts.getSignedInUser());
let result = { setup };
if (!setup) {
this.sendPageCallback(aMessageManager, aCallbackID, result);
return;
}
// We are signed in so need to build a richer result.
let devices = fxAccounts.device.recentDeviceList;
// A recent device list is fine, but if we don't even have that we should
// wait for it to be fetched.
if (!devices) {
await fxAccounts.device.refreshDeviceList();
devices = fxAccounts.device.recentDeviceList;
}
if (devices) {
// A falsey `devices` should be impossible, so we omit `devices` from
// the result object so the consuming page can try to differentiate
// between "no additional devices" and "something's wrong"
result.numOtherDevices = Math.max(0, devices.length - 1);
result.numDevicesByType = devices
.filter(d => !d.isCurrentDevice)
.reduce((accum, d) => {
let type = d.type || "unknown";
accum[type] = (accum[type] || 0) + 1;
return accum;
}, {});
}
// Each of the "browser services" - currently only "sync" is supported.
result.browserServices = {};
let hasSync = Services.prefs.prefHasUserValue("services.sync.username");
if (hasSync) {
result.browserServices.sync = {
// We always include 'setup' for b/w compatibility.
setup: true,
desktopDevices: Services.prefs.getIntPref(
"services.sync.clients.devices.desktop",
0
),
mobileDevices: Services.prefs.getIntPref(
"services.sync.clients.devices.mobile",
0
),
totalDevices: Services.prefs.getIntPref(
"services.sync.numClients",
0
),
};
}
// Each of the "account services", which we turn into a map keyed by ID.
let attachedClients = await fxAccounts.listAttachedOAuthClients();
result.accountServices = attachedClients
.filter(c => !!c.id)
.reduce((accum, c) => {
accum[c.id] = {
id: c.id,
lastAccessedWeeksAgo: c.lastAccessedDaysAgo
? Math.floor(c.lastAccessedDaysAgo / 7)
: null,
};
return accum;
}, {});
this.sendPageCallback(aMessageManager, aCallbackID, result);
})().catch(err => {
log.error(err);
this.sendPageCallback(aMessageManager, aCallbackID, {});
});
},
getAppInfo(aMessageManager, aWindow, aCallbackID) {
(async () => {
let appinfo = { version: Services.appinfo.version };

Просмотреть файл

@ -15,6 +15,7 @@ support-files =
skip-if = (verify && !debug && (os == 'linux'))
[browser_fxa.js]
skip-if = debug || asan # updateUI leaks
[browser_fxa_config.js]
[browser_no_tabs.js]
[browser_openPreferences.js]
[browser_openSearchPanel.js]

Просмотреть файл

@ -0,0 +1,239 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
var gTestTab;
var gContentAPI;
var gContentWindow;
add_task(setup_UITourTest);
add_UITour_task(async function test_no_user() {
const sandbox = sinon.createSandbox();
sandbox.stub(fxAccounts, "getSignedInUser").returns(null);
let result = await getConfigurationPromise("fxa");
Assert.deepEqual(result, { setup: false });
sandbox.restore();
});
add_UITour_task(async function test_no_sync_no_devices() {
const sandbox = sinon.createSandbox();
sandbox
.stub(fxAccounts, "getSignedInUser")
.returns({ email: "foo@example.com" });
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => {
return [
{
id: 1,
name: "This Device",
isCurrentDevice: true,
type: "desktop",
},
];
});
sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([]);
let result = await getConfigurationPromise("fxa");
Assert.deepEqual(result, {
setup: true,
numOtherDevices: 0,
numDevicesByType: {},
accountServices: {},
browserServices: {},
});
sandbox.restore();
});
add_UITour_task(async function test_no_sync_many_devices() {
const sandbox = sinon.createSandbox();
sandbox
.stub(fxAccounts, "getSignedInUser")
.returns({ email: "foo@example.com" });
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => {
return [
{
id: 1,
name: "This Device",
isCurrentDevice: true,
type: "desktop",
},
{
id: 2,
name: "Other Device",
type: "mobile",
},
{
id: 3,
name: "My phone",
type: "phone",
},
{
id: 4,
name: "Who knows?",
},
{
id: 5,
name: "Another desktop",
type: "desktop",
},
{
id: 6,
name: "Yet Another desktop",
type: "desktop",
},
];
});
sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([]);
let result = await getConfigurationPromise("fxa");
Assert.deepEqual(result, {
setup: true,
accountServices: {},
browserServices: {},
numOtherDevices: 5,
numDevicesByType: {
desktop: 2,
mobile: 1,
phone: 1,
unknown: 1,
},
});
sandbox.restore();
});
add_UITour_task(async function test_no_sync_no_cached_devices() {
const sandbox = sinon.createSandbox();
sandbox
.stub(fxAccounts, "getSignedInUser")
.returns({ email: "foo@example.com" });
let devicesStub = sandbox.stub(fxAccounts.device, "recentDeviceList");
devicesStub.get(() => {
// Sinon doesn't seem to support second `getters` returning a different
// value, so replace the getter here.
devicesStub.get(() => {
return [
{
id: 1,
name: "This Device",
isCurrentDevice: true,
type: "desktop",
},
{
id: 2,
name: "Other Device",
type: "mobile",
},
];
});
// and here we want to say "nothing is yet cached"
return null;
});
sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([]);
let rdlStub = sandbox.stub(fxAccounts.device, "refreshDeviceList").resolves();
let result = await getConfigurationPromise("fxa");
Assert.deepEqual(result, {
setup: true,
accountServices: {},
browserServices: {},
numOtherDevices: 1,
numDevicesByType: {
mobile: 1,
},
});
Assert.ok(rdlStub.called);
sandbox.restore();
});
add_UITour_task(async function test_account_clients() {
const sandbox = sinon.createSandbox();
sandbox
.stub(fxAccounts, "getSignedInUser")
.returns({ email: "foo@example.com" });
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => []);
sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([
{
id: "802d56ef2a9af9fa",
lastAccessedDaysAgo: 2,
},
{
id: "1f30e32975ae5112",
lastAccessedDaysAgo: 10,
},
{
id: null,
name: "Some browser",
lastAccessedDaysAgo: 10,
},
{
id: "null-last-accessed",
lastAccessedDaysAgo: null,
},
]);
Assert.deepEqual(await getConfigurationPromise("fxa"), {
setup: true,
numOtherDevices: 0,
numDevicesByType: {},
accountServices: {
"802d56ef2a9af9fa": {
id: "802d56ef2a9af9fa",
lastAccessedWeeksAgo: 0,
},
"1f30e32975ae5112": {
id: "1f30e32975ae5112",
lastAccessedWeeksAgo: 1,
},
"null-last-accessed": {
id: "null-last-accessed",
lastAccessedWeeksAgo: null,
},
},
browserServices: {},
});
sandbox.restore();
});
add_UITour_task(async function test_sync() {
const sandbox = sinon.createSandbox();
sandbox
.stub(fxAccounts, "getSignedInUser")
.returns({ email: "foo@example.com" });
sandbox.stub(fxAccounts.device, "recentDeviceList").get(() => []);
sandbox.stub(fxAccounts, "listAttachedOAuthClients").resolves([]);
Services.prefs.setCharPref("services.sync.username", "tests@mozilla.org");
Services.prefs.setIntPref("services.sync.clients.devices.desktop", 4);
Services.prefs.setIntPref("services.sync.clients.devices.mobile", 5);
Services.prefs.setIntPref("services.sync.numClients", 9);
Assert.deepEqual(await getConfigurationPromise("fxa"), {
setup: true,
numOtherDevices: 0,
numDevicesByType: {},
accountServices: {},
browserServices: {
sync: {
setup: true,
mobileDevices: 5,
desktopDevices: 4,
totalDevices: 9,
},
},
});
Services.prefs.clearUserPref("services.sync.username");
Services.prefs.clearUserPref("services.sync.clients.devices.desktop");
Services.prefs.clearUserPref("services.sync.clients.devices.mobile");
Services.prefs.clearUserPref("services.sync.numClients");
sandbox.restore();
});
add_UITour_task(async function test_fxa_fails() {
const sandbox = sinon.createSandbox();
sandbox.stub(fxAccounts, "getSignedInUser").throws();
let result = await getConfigurationPromise("fxa");
Assert.deepEqual(result, {});
sandbox.restore();
});

Просмотреть файл

@ -435,40 +435,37 @@ class FxAccounts {
}
/**
* Returns an array listing all the OAuth clients
* connected to the authenticated user's account.
* Devices and web sessions are not included.
* Returns an array listing all the OAuth clients connected to the
* authenticated user's account. This includes browsers and web sessions - no
* filtering is done of the set returned by the FxA server.
*
* @typedef {Object} AttachedClient
* @property {String} id - OAuth `client_id` of the client.
* @property {String} name - Client name. e.g. Firefox Monitor.
* @property {Number} lastAccessTime - Last access time in milliseconds.
* @property {Number} lastAccessedDaysAgo - How many days ago the client last
* accessed the FxA server APIs.
*
* @returns {Array.<AttachedClient>} A list of attached clients.
*/
async listAttachedOAuthClients() {
// We expose last accessed times in 'days ago'
const ONE_DAY = 24 * 60 * 60 * 1000;
return this._withVerifiedAccountState(async state => {
const { sessionToken } = await state.getUserAccountData(["sessionToken"]);
const attachedClients = await this._internal.fxAccountsClient.attachedClients(
sessionToken
);
return attachedClients.reduce((oauthClients, client) => {
// This heuristic aims to keep tokens for "associated services"
// while throwing away the "browser" ones.
if (
client.clientId &&
!client.deviceId &&
!client.sessionTokenId &&
client.scope
) {
oauthClients.push({
// We should use the server timestamp here - bug 1595635
let now = Date.now();
return attachedClients.map(client => {
const daysAgo = client.lastAccessTime
? Math.max(Math.floor((now - client.lastAccessTime) / ONE_DAY), 0)
: null;
return {
id: client.clientId,
name: client.name,
lastAccessTime: client.lastAccessTime,
lastAccessedDaysAgo: daysAgo,
};
});
}
return oauthClients;
}, []);
});
}

Просмотреть файл

@ -1600,6 +1600,9 @@ add_task(async function test_getOAuthToken_authErrorRefreshesCertificate() {
});
add_task(async function test_listAttachedOAuthClients() {
const ONE_HOUR = 60 * 60 * 1000;
const ONE_DAY = 24 * ONE_HOUR;
let fxa = new MockFxAccounts();
let alice = getTestUser("alice");
alice.verified = true;
@ -1607,29 +1610,14 @@ add_task(async function test_listAttachedOAuthClients() {
let client = fxa._internal.fxAccountsClient;
client.attachedClients = async () => {
return [
{
clientId: null,
deviceId: "deadbeef",
sessionTokenId: "deadbeef",
name: "Good ol' desktop device",
scope: null,
lastAccessTime: 1569263031001,
},
{
clientId: null,
deviceId: null,
sessionTokenId: "deadbeef",
name: "Mobile device w/ no device record",
scope: null,
lastAccessTime: 1569263031001,
},
// This entry was previously filtered but no longer is!
{
clientId: "a2270f727f45f648",
deviceId: "deadbeef",
sessionTokenId: null,
name: "Firefox Preview (no session token)",
scope: ["profile", "https://identity.mozilla.com/apps/oldsync"],
lastAccessTime: 1569263031001,
lastAccessTime: Date.now(),
},
{
clientId: "802d56ef2a9af9fa",
@ -1637,7 +1625,7 @@ add_task(async function test_listAttachedOAuthClients() {
sessionTokenId: null,
name: "Firefox Monitor",
scope: ["profile"],
lastAccessTime: 1569263031000,
lastAccessTime: Date.now() - ONE_DAY - ONE_HOUR,
},
{
clientId: "1f30e32975ae5112",
@ -1645,7 +1633,23 @@ add_task(async function test_listAttachedOAuthClients() {
sessionTokenId: null,
name: "Firefox Send",
scope: ["profile", "https://identity.mozilla.com/apps/send"],
lastAccessTime: 1569263013000,
lastAccessTime: Date.now() - ONE_DAY * 2 - ONE_HOUR,
},
// One with a future date should be impossible, but having a negative
// result here would almost certainly confuse something!
{
clientId: "future-date",
deviceId: null,
sessionTokenId: null,
name: "Whatever",
lastAccessTime: Date.now() + ONE_DAY,
},
// A missing/null lastAccessTime should end up with a missing lastAccessedDaysAgo
{
clientId: "missing-date",
deviceId: null,
sessionTokenId: null,
name: "Whatever",
},
];
};
@ -1653,15 +1657,25 @@ add_task(async function test_listAttachedOAuthClients() {
await fxa.setSignedInUser(alice);
const clients = await fxa.listAttachedOAuthClients();
Assert.deepEqual(clients, [
{
id: "a2270f727f45f648",
lastAccessedDaysAgo: 0,
},
{
id: "802d56ef2a9af9fa",
name: "Firefox Monitor",
lastAccessTime: 1569263031000,
lastAccessedDaysAgo: 1,
},
{
id: "1f30e32975ae5112",
name: "Firefox Send",
lastAccessTime: 1569263013000,
lastAccessedDaysAgo: 2,
},
{
id: "future-date",
lastAccessedDaysAgo: 0,
},
{
id: "missing-date",
lastAccessedDaysAgo: null,
},
]);
});