summaryrefslogtreecommitdiffstats
path: root/services/sync/tests/unit/test_fxa_migration.js
blob: 7c65d5996624336ba136d04717dff2fd59596adc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
// Test the FxAMigration module
Cu.import("resource://services-sync/FxaMigrator.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/FxAccounts.jsm");
Cu.import("resource://gre/modules/FxAccountsCommon.js");
Cu.import("resource://services-sync/browserid_identity.js");

// Set our username pref early so sync initializes with the legacy provider.
Services.prefs.setCharPref("services.sync.username", "foo");
// And ensure all debug messages end up being printed.
Services.prefs.setCharPref("services.sync.log.appender.dump", "Debug");

// Now import sync
Cu.import("resource://services-sync/service.js");
Cu.import("resource://services-sync/record.js");
Cu.import("resource://services-sync/util.js");

// And reset the username.
Services.prefs.clearUserPref("services.sync.username");

Cu.import("resource://testing-common/services/sync/utils.js");
Cu.import("resource://testing-common/services/common/logging.js");
Cu.import("resource://testing-common/services/sync/rotaryengine.js");

const FXA_USERNAME = "someone@somewhere";

// Utilities
function promiseOneObserver(topic) {
  return new Promise((resolve, reject) => {
    let observer = function(subject, topic, data) {
      Services.obs.removeObserver(observer, topic);
      resolve({ subject: subject, data: data });
    }
    Services.obs.addObserver(observer, topic, false);
  });
}

function promiseStopServer(server) {
  return new Promise((resolve, reject) => {
    server.stop(resolve);
  });
}


// Helpers
function configureLegacySync() {
  let engine = new RotaryEngine(Service);
  engine.enabled = true;
  Svc.Prefs.set("registerEngines", engine.name);
  Svc.Prefs.set("log.logger.engine.rotary", "Trace");

  let contents = {
    meta: {global: {engines: {rotary: {version: engine.version,
                                       syncID:  engine.syncID}}}},
    crypto: {},
    rotary: {}
  };

  const USER = "foo";
  const PASSPHRASE = "abcdeabcdeabcdeabcdeabcdea";

  setBasicCredentials(USER, "password", PASSPHRASE);

  let onRequest = function(request, response) {
    // ideally we'd only do this while a legacy user is configured, but WTH.
    response.setHeader("x-weave-alert", JSON.stringify({code: "soft-eol"}));
  }
  let server = new SyncServer({onRequest: onRequest});
  server.registerUser(USER, "password");
  server.createContents(USER, contents);
  server.start();

  Service.serverURL = server.baseURI;
  Service.clusterURL = server.baseURI;
  Service.identity.username = USER;
  Service._updateCachedURLs();

  Service.engineManager._engines[engine.name] = engine;

  return [engine, server];
}

function configureFxa() {
  Services.prefs.setCharPref("identity.fxaccounts.auth.uri", "http://localhost");
}

add_task(function *testMigration() {
  configureFxa();

  // when we do a .startOver we want the new provider.
  let oldValue = Services.prefs.getBoolPref("services.sync-testing.startOverKeepIdentity");
  Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", false);

  // disable the addons engine - this engine choice is arbitrary, but we
  // want to check it remains disabled after migration.
  Services.prefs.setBoolPref("services.sync.engine.addons", false);

  do_register_cleanup(() => {
    Services.prefs.setBoolPref("services.sync-testing.startOverKeepIdentity", oldValue)
    Services.prefs.setBoolPref("services.sync.engine.addons", true);
  });

  // No sync user - that should report no user-action necessary.
  Assert.deepEqual((yield fxaMigrator._queueCurrentUserState()), null,
                   "no user state when complete");

  // Arrange for a legacy sync user and manually bump the migrator
  let [engine, server] = configureLegacySync();

  // Check our disabling of the "addons" engine worked, and for good measure,
  // that the "passwords" engine is enabled.
  Assert.ok(!Service.engineManager.get("addons").enabled, "addons is disabled");
  Assert.ok(Service.engineManager.get("passwords").enabled, "passwords is enabled");

  // monkey-patch the migration sentinel code so we know it was called.
  let haveStartedSentinel = false;
  let origSetFxAMigrationSentinel = Service.setFxAMigrationSentinel;
  let promiseSentinelWritten = new Promise((resolve, reject) => {
    Service.setFxAMigrationSentinel = function(arg) {
      haveStartedSentinel = true;
      return origSetFxAMigrationSentinel.call(Service, arg).then(result => {
        Service.setFxAMigrationSentinel = origSetFxAMigrationSentinel;
        resolve(result);
        return result;
      });
    }
  });

  // We are now configured for legacy sync, but we aren't in an EOL state yet,
  // so should still be not waiting for a user.
  Assert.deepEqual((yield fxaMigrator._queueCurrentUserState()), null,
                   "no user state before server EOL");

  // Start a sync - this will cause an EOL notification which the migrator's
  // observer will notice.
  let promise = promiseOneObserver("fxa-migration:state-changed");
  _("Starting sync");
  Service.sync();
  _("Finished sync");

  // We should have seen the observer, so be waiting for an FxA user.
  Assert.equal((yield promise).data, fxaMigrator.STATE_USER_FXA, "now waiting for FxA.")

  // Re-calling our user-state promise should also reflect the same state.
  Assert.equal((yield fxaMigrator._queueCurrentUserState()),
               fxaMigrator.STATE_USER_FXA,
               "still waiting for FxA.");

  // arrange for an unverified FxA user.
  let config = makeIdentityConfig({username: FXA_USERNAME});
  let fxa = new FxAccounts({});
  config.fxaccount.user.email = config.username;
  delete config.fxaccount.user.verified;
  // *sob* - shouldn't need this boilerplate
  fxa.internal.currentAccountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
    this.cert = {
      validUntil: fxa.internal.now() + CERT_LIFETIME,
      cert: "certificate",
    };
    return Promise.resolve(this.cert.cert);
  };

  // As soon as we set the FxA user the observers should fire and magically
  // transition.
  promise = promiseOneObserver("fxa-migration:state-changed");
  fxAccounts.setSignedInUser(config.fxaccount.user);

  let observerInfo = yield promise;
  Assert.equal(observerInfo.data,
               fxaMigrator.STATE_USER_FXA_VERIFIED,
               "now waiting for verification");
  Assert.ok(observerInfo.subject instanceof Ci.nsISupportsString,
            "email was passed to observer");
  Assert.equal(observerInfo.subject.data,
               FXA_USERNAME,
               "email passed to observer is correct");

  // should have seen the user set, so state should automatically update.
  Assert.equal((yield fxaMigrator._queueCurrentUserState()),
               fxaMigrator.STATE_USER_FXA_VERIFIED,
               "now waiting for verification");

  // Before we verify the user, fire off a sync that calls us back during
  // the sync and before it completes - this way we can ensure we do the right
  // thing in terms of blocking sync and waiting for it to complete.

  let wasWaiting = false;
  // This is a PITA as sync is pseudo-blocking.
  engine._syncFinish = function () {
    // We aren't in a generator here, so use a helper to block on promises.
    function getState() {
      let cb = Async.makeSpinningCallback();
      fxaMigrator._queueCurrentUserState().then(state => cb(null, state));
      return cb.wait();
    }
    // should still be waiting for verification.
    Assert.equal(getState(), fxaMigrator.STATE_USER_FXA_VERIFIED,
                 "still waiting for verification");

    // arrange for the user to be verified.  The fxAccount's mock story is
    // broken, so go behind its back.
    config.fxaccount.user.verified = true;
    fxAccounts.setSignedInUser(config.fxaccount.user);
    Services.obs.notifyObservers(null, ONVERIFIED_NOTIFICATION, null);

    // spinningly wait for the migrator to catch up - sync is running so
    // we should be in a 'null' user-state as there is no user-action
    // necessary.
    let cb = Async.makeSpinningCallback();
    promiseOneObserver("fxa-migration:state-changed").then(({ data: state }) => cb(null, state));
    Assert.equal(cb.wait(), null, "no user action necessary while sync completes.");

    // We must not have started writing the sentinel yet.
    Assert.ok(!haveStartedSentinel, "haven't written a sentinel yet");

    // sync should be blocked from continuing
    Assert.ok(Service.scheduler.isBlocked, "sync is blocked.")

    wasWaiting = true;
    throw ex;
  };

  _("Starting sync");
  Service.sync();
  _("Finished sync");

  // mock sync so we can ensure the final sync is scheduled with the FxA user.
  // (letting a "normal" sync complete is a PITA without mocking huge amounts
  // of FxA infra)
  let promiseFinalSync = new Promise((resolve, reject) => {
    let oldSync = Service.sync;
    Service.sync = function() {
      Service.sync = oldSync;
      resolve();
    }
  });

  Assert.ok(wasWaiting, "everything was good while sync was running.")

  // The migration is now going to run to completion.
  // sync should still be "blocked"
  Assert.ok(Service.scheduler.isBlocked, "sync is blocked.");

  // We should see the migration sentinel written and it should return true.
  Assert.ok((yield promiseSentinelWritten), "wrote the sentinel");

  // And we should see a new sync start
  yield promiseFinalSync;

  // and we should be configured for FxA
  let WeaveService = Cc["@mozilla.org/weave/service;1"]
         .getService(Components.interfaces.nsISupports)
         .wrappedJSObject;
  Assert.ok(WeaveService.fxAccountsEnabled, "FxA is enabled");
  Assert.ok(Service.identity instanceof BrowserIDManager,
            "sync is configured with the browserid_identity provider.");
  Assert.equal(Service.identity.username, config.username, "correct user configured")
  Assert.ok(!Service.scheduler.isBlocked, "sync is not blocked.")
  // and the user state should remain null.
  Assert.deepEqual((yield fxaMigrator._queueCurrentUserState()),
                   null,
                   "still no user action necessary");
  // and our engines should be in the same enabled/disabled state as before.
  Assert.ok(!Service.engineManager.get("addons").enabled, "addons is still disabled");
  Assert.ok(Service.engineManager.get("passwords").enabled, "passwords is still enabled");

  // aaaand, we are done - clean up.
  yield promiseStopServer(server);
});


function run_test() {
  initTestLogging();
  do_register_cleanup(() => {
    fxaMigrator.finalize();
    Svc.Prefs.resetBranch("");
  });
  run_next_test();
}