Bug 1548404 - Update UITour to reflect the decoupling of FxA and Sync. r=MattN,andreio,rfkelly
authorMark Hammond <mhammond@skippinet.com.au>
Tue, 12 Nov 2019 23:58:03 +0000
changeset 501657 c5c7e5fc0307caa3a6e455a98ccfa7a5b9b4459a
parent 501656 8fdc7a34e665168724f2c9b51188df4cbab27fac
child 501658 8daa186bd18b8a894f95e22f32c9ecc45481553a
push id114172
push userdluca@mozilla.com
push dateTue, 19 Nov 2019 11:31:10 +0000
treeherdermozilla-inbound@b5c5ba07d3db [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN, andreio, rfkelly
bugs1548404
milestone72.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1548404 - Update UITour to reflect the decoupling of FxA and Sync. r=MattN,andreio,rfkelly Differential Revision: https://phabricator.services.mozilla.com/D51976
browser/components/uitour/UITour-lib.js
browser/components/uitour/UITour.jsm
browser/components/uitour/test/browser.ini
browser/components/uitour/test/browser_fxa_config.js
services/fxaccounts/FxAccounts.jsm
services/fxaccounts/tests/xpcshell/test_accounts.js
--- a/browser/components/uitour/UITour-lib.js
+++ b/browser/components/uitour/UITour-lib.js
@@ -382,16 +382,18 @@ if (typeof Mozilla == "undefined") {
    * @description Valid values:<ul>
    * <li>{@link Mozilla.UITour.Configuration.AppInfo|appinfo}</li>
    * <li>{@link Mozilla.UITour.Configuration.CanReset|canReset}</li>
    * <li>{@link Mozilla.UITour.Configuration.AvailableTargets|availableTargets}</li>
    * <li>{@link Mozilla.UITour.Configuration.Search|search}</li>
    * <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>
    */
 
   /**
    * @namespace Mozilla.UITour.Configuration
    * @see Mozilla.UITour.getConfiguration
    * @see Mozilla.UITour.ConfigurationName
    */
@@ -444,16 +446,75 @@ if (typeof Mozilla == "undefined") {
    * @property {Boolean} setup - Whether sync is setup
    * @property {Number} desktopDevices - Number of desktop devices
    * @property {Number} mobileDevices - Number of mobile devices
    * @property {Number} totalDevices - Total number of connected devices
    * @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
    */
 
   /**
    * Retrieve some information about the application/profile.
    *
    * @param {Mozilla.UITour.ConfigurationName} configName - Name of configuration to retrieve
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -21,16 +21,21 @@ XPCOMUtils.defineLazyGlobalGetters(this,
 
 ChromeUtils.defineModuleGetter(
   this,
   "CustomizableUI",
   "resource:///modules/CustomizableUI.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
+  "fxAccounts",
+  "resource://gre/modules/FxAccounts.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
   "FxAccounts",
   "resource://gre/modules/FxAccounts.jsm"
 );
 ChromeUtils.defineModuleGetter(
   this,
   "PageActions",
   "resource:///modules/PageActions.jsm"
 );
@@ -1664,16 +1669,23 @@ var UITour = {
           })
           .catch(() => {
             this.sendPageCallback(aMessageManager, aCallbackID, {
               engines: [],
               searchEngineIdentifier: "",
             });
           });
         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"),
           desktopDevices: Services.prefs.getIntPref(
             "services.sync.clients.devices.desktop",
             0
           ),
           mobileDevices: Services.prefs.getIntPref(
@@ -1716,16 +1728,87 @@ var UITour = {
       default:
         log.error(
           "setConfiguration: Unknown configuration requested: " + aConfiguration
         );
         break;
     }
   },
 
+  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 };
 
       // Identifier of the partner repack, as stored in preference "distribution.id"
       // and included in Firefox and other update pings. Note this is not the same as
       // Services.appinfo.distributionID (value of MOZ_DISTRIBUTION_ID is set at build time).
       let distribution = Services.prefs
--- a/browser/components/uitour/test/browser.ini
+++ b/browser/components/uitour/test/browser.ini
@@ -10,16 +10,17 @@ support-files =
   uitour.html
   ../UITour-lib.js
 
 [browser_backgroundTab.js]
 [browser_closeTab.js]
 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]
 skip-if = true # Bug 1113038 - Intermittent "Popup was opened"
 [browser_showMenu.js]
 [browser_UITour.js]
 skip-if = os == "linux" || verify # Intermittent failures, bug 951965
 [browser_UITour2.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/uitour/test/browser_fxa_config.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();
+});
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -430,50 +430,47 @@ class FxAccounts {
     return this._internal.withCurrentAccountState(func);
   }
 
   _withVerifiedAccountState(func) {
     return this._internal.withVerifiedAccountState(func);
   }
 
   /**
-   * 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({
-            id: client.clientId,
-            name: client.name,
-            lastAccessTime: client.lastAccessTime,
-          });
-        }
-        return oauthClients;
-      }, []);
+      // 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,
+          lastAccessedDaysAgo: daysAgo,
+        };
+      });
     });
   }
 
   /**
    * Retrieves an OAuth authorization code.
    *
    * @param {Object} options
    * @param options.client_id
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -1595,78 +1595,92 @@ add_task(async function test_getOAuthTok
 
   Assert.equal(result, "token");
 
   Assert.equal(numTokenCalls, 2);
   Assert.equal(fxa._internal._getCertificateSigned_calls.length, 2);
 });
 
 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;
 
   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",
         deviceId: null,
         sessionTokenId: null,
         name: "Firefox Monitor",
         scope: ["profile"],
-        lastAccessTime: 1569263031000,
+        lastAccessTime: Date.now() - ONE_DAY - ONE_HOUR,
       },
       {
         clientId: "1f30e32975ae5112",
         deviceId: null,
         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",
       },
     ];
   };
 
   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,
     },
   ]);
 });
 
 add_task(async function test_getSignedInUserProfile() {
   let alice = getTestUser("alice");
   alice.verified = true;