Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* Any copyright is dedicated to the Public Domain.
2 * http://creativecommons.org/publicdomain/zero/1.0/ */
4 "use strict";
6 Cu.import("resource://services-common/utils.js");
7 Cu.import("resource://gre/modules/Services.jsm");
8 Cu.import("resource://gre/modules/FxAccounts.jsm");
9 Cu.import("resource://gre/modules/FxAccountsClient.jsm");
10 Cu.import("resource://gre/modules/FxAccountsCommon.js");
11 Cu.import("resource://gre/modules/Promise.jsm");
12 Cu.import("resource://gre/modules/Log.jsm");
14 const ONE_HOUR_MS = 1000 * 60 * 60;
15 const ONE_DAY_MS = ONE_HOUR_MS * 24;
16 const TWO_MINUTES_MS = 1000 * 60 * 2;
18 initTestLogging("Trace");
20 // XXX until bug 937114 is fixed
21 Cu.importGlobalProperties(['atob']);
23 let log = Log.repository.getLogger("Services.FxAccounts.test");
24 log.level = Log.Level.Debug;
26 // See verbose logging from FxAccounts.jsm
27 Services.prefs.setCharPref("identity.fxaccounts.loglevel", "DEBUG");
29 function run_test() {
30 run_next_test();
31 }
33 /*
34 * The FxAccountsClient communicates with the remote Firefox
35 * Accounts auth server. Mock the server calls, with a little
36 * lag time to simulate some latency.
37 *
38 * We add the _verified attribute to mock the change in verification
39 * state on the FXA server.
40 */
41 function MockFxAccountsClient() {
42 this._email = "nobody@example.com";
43 this._verified = false;
45 // mock calls up to the auth server to determine whether the
46 // user account has been verified
47 this.recoveryEmailStatus = function (sessionToken) {
48 // simulate a call to /recovery_email/status
49 let deferred = Promise.defer();
51 let response = {
52 email: this._email,
53 verified: this._verified
54 };
55 deferred.resolve(response);
57 return deferred.promise;
58 };
60 this.accountKeys = function (keyFetchToken) {
61 let deferred = Promise.defer();
63 do_timeout(50, () => {
64 let response = {
65 kA: expandBytes("11"),
66 wrapKB: expandBytes("22")
67 };
68 deferred.resolve(response);
69 });
70 return deferred.promise;
71 };
73 this.resendVerificationEmail = function(sessionToken) {
74 // Return the session token to show that we received it in the first place
75 return Promise.resolve(sessionToken);
76 };
78 this.signCertificate = function() { throw "no" };
80 this.signOut = function() { return Promise.resolve(); };
82 FxAccountsClient.apply(this);
83 }
84 MockFxAccountsClient.prototype = {
85 __proto__: FxAccountsClient.prototype
86 }
88 let MockStorage = function() {
89 this.data = null;
90 };
91 MockStorage.prototype = Object.freeze({
92 set: function (contents) {
93 this.data = contents;
94 return Promise.resolve(null);
95 },
96 get: function () {
97 return Promise.resolve(this.data);
98 },
99 });
101 /*
102 * We need to mock the FxAccounts module's interfaces to external
103 * services, such as storage and the FxAccounts client. We also
104 * mock the now() method, so that we can simulate the passing of
105 * time and verify that signatures expire correctly.
106 */
107 function MockFxAccounts() {
108 return new FxAccounts({
109 _getCertificateSigned_calls: [],
110 _d_signCertificate: Promise.defer(),
111 _now_is: new Date(),
112 signedInUserStorage: new MockStorage(),
113 now: function () {
114 return this._now_is;
115 },
116 getCertificateSigned: function (sessionToken, serializedPublicKey) {
117 _("mock getCertificateSigned\n");
118 this._getCertificateSigned_calls.push([sessionToken, serializedPublicKey]);
119 return this._d_signCertificate.promise;
120 },
121 fxAccountsClient: new MockFxAccountsClient()
122 });
123 }
125 add_test(function test_non_https_remote_server_uri() {
126 Services.prefs.setCharPref(
127 "identity.fxaccounts.remote.signup.uri",
128 "http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html");
129 do_check_throws_message(function () {
130 fxAccounts.getAccountsSignUpURI();
131 }, "Firefox Accounts server must use HTTPS");
133 Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri");
135 run_next_test();
136 });
138 add_task(function test_get_signed_in_user_initially_unset() {
139 // This test, unlike the rest, uses an un-mocked FxAccounts instance.
140 // However, we still need to pass an object to the constructor to
141 // force it to expose "internal", so we can test the disk storage.
142 let account = new FxAccounts({onlySetInternal: true})
143 let credentials = {
144 email: "foo@example.com",
145 uid: "1234@lcip.org",
146 assertion: "foobar",
147 sessionToken: "dead",
148 kA: "beef",
149 kB: "cafe",
150 verified: true
151 };
153 let result = yield account.getSignedInUser();
154 do_check_eq(result, null);
156 yield account.setSignedInUser(credentials);
158 let result = yield account.getSignedInUser();
159 do_check_eq(result.email, credentials.email);
160 do_check_eq(result.assertion, credentials.assertion);
161 do_check_eq(result.kB, credentials.kB);
163 // Delete the memory cache and force the user
164 // to be read and parsed from storage (e.g. disk via JSONStorage).
165 delete account.internal.signedInUser;
166 let result = yield account.getSignedInUser();
167 do_check_eq(result.email, credentials.email);
168 do_check_eq(result.assertion, credentials.assertion);
169 do_check_eq(result.kB, credentials.kB);
171 // sign out
172 let localOnly = true;
173 yield account.signOut(localOnly);
175 // user should be undefined after sign out
176 let result = yield account.getSignedInUser();
177 do_check_eq(result, null);
178 });
180 // Sanity-check that our mocked client is working correctly
181 add_test(function test_client_mock() {
182 do_test_pending();
184 let fxa = new MockFxAccounts();
185 let client = fxa.internal.fxAccountsClient;
186 do_check_eq(client._verified, false);
187 do_check_eq(typeof client.signIn, "function");
189 // The recoveryEmailStatus function eventually fulfills its promise
190 client.recoveryEmailStatus()
191 .then(response => {
192 do_check_eq(response.verified, false);
193 do_test_finished();
194 run_next_test();
195 });
196 });
198 // Sign in a user, and after a little while, verify the user's email.
199 // Right after signing in the user, we should get the 'onlogin' notification.
200 // Polling should detect that the email is verified, and eventually
201 // 'onverified' should be observed
202 add_test(function test_verification_poll() {
203 do_test_pending();
205 let fxa = new MockFxAccounts();
206 let test_user = getTestUser("francine");
207 let login_notification_received = false;
209 makeObserver(ONVERIFIED_NOTIFICATION, function() {
210 log.debug("test_verification_poll observed onverified");
211 // Once email verification is complete, we will observe onverified
212 fxa.internal.getUserAccountData().then(user => {
213 // And confirm that the user's state has changed
214 do_check_eq(user.verified, true);
215 do_check_eq(user.email, test_user.email);
216 do_check_true(login_notification_received);
217 do_test_finished();
218 run_next_test();
219 });
220 });
222 makeObserver(ONLOGIN_NOTIFICATION, function() {
223 log.debug("test_verification_poll observer onlogin");
224 login_notification_received = true;
225 });
227 fxa.setSignedInUser(test_user).then(() => {
228 fxa.internal.getUserAccountData().then(user => {
229 // The user is signing in, but email has not been verified yet
230 do_check_eq(user.verified, false);
231 do_timeout(200, function() {
232 log.debug("Mocking verification of francine's email");
233 fxa.internal.fxAccountsClient._email = test_user.email;
234 fxa.internal.fxAccountsClient._verified = true;
235 });
236 });
237 });
238 });
240 // Sign in the user, but never verify the email. The check-email
241 // poll should time out. No verifiedlogin event should be observed, and the
242 // internal whenVerified promise should be rejected
243 add_test(function test_polling_timeout() {
244 do_test_pending();
246 // This test could be better - the onverified observer might fire on
247 // somebody else's stack, and we're not making sure that we're not receiving
248 // such a message. In other words, this tests either failure, or success, but
249 // not both.
251 let fxa = new MockFxAccounts();
252 let test_user = getTestUser("carol");
254 let removeObserver = makeObserver(ONVERIFIED_NOTIFICATION, function() {
255 do_throw("We should not be getting a login event!");
256 });
258 fxa.internal.POLL_SESSION = 1;
259 fxa.internal.POLL_STEP = 2;
261 let p = fxa.internal.whenVerified({});
263 fxa.setSignedInUser(test_user).then(() => {
264 p.then(
265 (success) => {
266 do_throw("this should not succeed");
267 },
268 (fail) => {
269 removeObserver();
270 do_test_finished();
271 run_next_test();
272 }
273 );
274 });
275 });
277 add_test(function test_getKeys() {
278 do_test_pending();
279 let fxa = new MockFxAccounts();
280 let user = getTestUser("eusebius");
282 // Once email has been verified, we will be able to get keys
283 user.verified = true;
285 fxa.setSignedInUser(user).then(() => {
286 fxa.getSignedInUser().then((user) => {
287 // Before getKeys, we have no keys
288 do_check_eq(!!user.kA, false);
289 do_check_eq(!!user.kB, false);
290 // And we still have a key-fetch token to use
291 do_check_eq(!!user.keyFetchToken, true);
293 fxa.internal.getKeys().then(() => {
294 fxa.getSignedInUser().then((user) => {
295 // Now we should have keys
296 do_check_eq(fxa.internal.isUserEmailVerified(user), true);
297 do_check_eq(!!user.verified, true);
298 do_check_eq(user.kA, expandHex("11"));
299 do_check_eq(user.kB, expandHex("66"));
300 do_check_eq(user.keyFetchToken, undefined);
301 do_test_finished();
302 run_next_test();
303 });
304 });
305 });
306 });
307 });
309 // fetchAndUnwrapKeys with no keyFetchToken should trigger signOut
310 add_test(function test_fetchAndUnwrapKeys_no_token() {
311 do_test_pending();
313 let fxa = new MockFxAccounts();
314 let user = getTestUser("lettuce.protheroe");
315 delete user.keyFetchToken
317 makeObserver(ONLOGOUT_NOTIFICATION, function() {
318 log.debug("test_fetchAndUnwrapKeys_no_token observed logout");
319 fxa.internal.getUserAccountData().then(user => {
320 do_test_finished();
321 run_next_test();
322 });
323 });
325 fxa.setSignedInUser(user).then((user) => {
326 fxa.internal.fetchAndUnwrapKeys();
327 });
328 });
330 // Alice (User A) signs up but never verifies her email. Then Bob (User B)
331 // signs in with a verified email. Ensure that no sign-in events are triggered
332 // on Alice's behalf. In the end, Bob should be the signed-in user.
333 add_test(function test_overlapping_signins() {
334 do_test_pending();
336 let fxa = new MockFxAccounts();
337 let alice = getTestUser("alice");
338 let bob = getTestUser("bob");
340 makeObserver(ONVERIFIED_NOTIFICATION, function() {
341 log.debug("test_overlapping_signins observed onverified");
342 // Once email verification is complete, we will observe onverified
343 fxa.internal.getUserAccountData().then(user => {
344 do_check_eq(user.email, bob.email);
345 do_check_eq(user.verified, true);
346 do_test_finished();
347 run_next_test();
348 });
349 });
351 // Alice is the user signing in; her email is unverified.
352 fxa.setSignedInUser(alice).then(() => {
353 log.debug("Alice signing in ...");
354 fxa.internal.getUserAccountData().then(user => {
355 do_check_eq(user.email, alice.email);
356 do_check_eq(user.verified, false);
357 log.debug("Alice has not verified her email ...");
359 // Now Bob signs in instead and actually verifies his email
360 log.debug("Bob signing in ...");
361 fxa.setSignedInUser(bob).then(() => {
362 do_timeout(200, function() {
363 // Mock email verification ...
364 log.debug("Bob verifying his email ...");
365 fxa.internal.fxAccountsClient._verified = true;
366 });
367 });
368 });
369 });
370 });
372 add_task(function test_getAssertion() {
373 let fxa = new MockFxAccounts();
375 do_check_throws(function() {
376 yield fxa.getAssertion("nonaudience");
377 });
379 let creds = {
380 sessionToken: "sessionToken",
381 kA: expandHex("11"),
382 kB: expandHex("66"),
383 verified: true
384 };
385 // By putting kA/kB/verified in "creds", we skip ahead
386 // to the "we're ready" stage.
387 yield fxa.setSignedInUser(creds);
389 _("== ready to go\n");
390 // Start with a nice arbitrary but realistic date. Here we use a nice RFC
391 // 1123 date string like we would get from an HTTP header. Over the course of
392 // the test, we will update 'now', but leave 'start' where it is.
393 let now = Date.parse("Mon, 13 Jan 2014 21:45:06 GMT");
394 let start = now;
395 fxa.internal._now_is = now;
397 let d = fxa.getAssertion("audience.example.com");
398 // At this point, a thread has been spawned to generate the keys.
399 _("-- back from fxa.getAssertion\n");
400 fxa.internal._d_signCertificate.resolve("cert1");
401 let assertion = yield d;
402 do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1);
403 do_check_eq(fxa.internal._getCertificateSigned_calls[0][0], "sessionToken");
404 do_check_neq(assertion, null);
405 _("ASSERTION: " + assertion + "\n");
406 let pieces = assertion.split("~");
407 do_check_eq(pieces[0], "cert1");
408 let keyPair = fxa.internal.currentAccountState.keyPair;
409 let cert = fxa.internal.currentAccountState.cert;
410 do_check_neq(keyPair, undefined);
411 _(keyPair.validUntil + "\n");
412 let p2 = pieces[1].split(".");
413 let header = JSON.parse(atob(p2[0]));
414 _("HEADER: " + JSON.stringify(header) + "\n");
415 do_check_eq(header.alg, "DS128");
416 let payload = JSON.parse(atob(p2[1]));
417 _("PAYLOAD: " + JSON.stringify(payload) + "\n");
418 do_check_eq(payload.aud, "audience.example.com");
419 do_check_eq(keyPair.validUntil, start + KEY_LIFETIME);
420 do_check_eq(cert.validUntil, start + CERT_LIFETIME);
421 _("delta: " + Date.parse(payload.exp - start) + "\n");
422 let exp = Number(payload.exp);
424 do_check_eq(exp, now + ASSERTION_LIFETIME);
426 // Reset for next call.
427 fxa.internal._d_signCertificate = Promise.defer();
429 // Getting a new assertion "soon" (i.e., w/o incrementing "now"), even for
430 // a new audience, should not provoke key generation or a signing request.
431 assertion = yield fxa.getAssertion("other.example.com");
433 // There were no additional calls - same number of getcert calls as before
434 do_check_eq(fxa.internal._getCertificateSigned_calls.length, 1);
436 // Wait an hour; assertion use period expires, but not the certificate
437 now += ONE_HOUR_MS;
438 fxa.internal._now_is = now;
440 // This won't block on anything - will make an assertion, but not get a
441 // new certificate.
442 assertion = yield fxa.getAssertion("third.example.com");
444 // Test will time out if that failed (i.e., if that had to go get a new cert)
445 pieces = assertion.split("~");
446 do_check_eq(pieces[0], "cert1");
447 p2 = pieces[1].split(".");
448 header = JSON.parse(atob(p2[0]));
449 payload = JSON.parse(atob(p2[1]));
450 do_check_eq(payload.aud, "third.example.com");
452 // The keypair and cert should have the same validity as before, but the
453 // expiration time of the assertion should be different. We compare this to
454 // the initial start time, to which they are relative, not the current value
455 // of "now".
457 keyPair = fxa.internal.currentAccountState.keyPair;
458 cert = fxa.internal.currentAccountState.cert;
459 do_check_eq(keyPair.validUntil, start + KEY_LIFETIME);
460 do_check_eq(cert.validUntil, start + CERT_LIFETIME);
461 exp = Number(payload.exp);
462 do_check_eq(exp, now + ASSERTION_LIFETIME);
464 // Now we wait even longer, and expect both assertion and cert to expire. So
465 // we will have to get a new keypair and cert.
466 now += ONE_DAY_MS;
467 fxa.internal._now_is = now;
468 d = fxa.getAssertion("fourth.example.com");
469 fxa.internal._d_signCertificate.resolve("cert2");
470 assertion = yield d;
471 do_check_eq(fxa.internal._getCertificateSigned_calls.length, 2);
472 do_check_eq(fxa.internal._getCertificateSigned_calls[1][0], "sessionToken");
473 pieces = assertion.split("~");
474 do_check_eq(pieces[0], "cert2");
475 p2 = pieces[1].split(".");
476 header = JSON.parse(atob(p2[0]));
477 payload = JSON.parse(atob(p2[1]));
478 do_check_eq(payload.aud, "fourth.example.com");
479 keyPair = fxa.internal.currentAccountState.keyPair;
480 cert = fxa.internal.currentAccountState.cert;
481 do_check_eq(keyPair.validUntil, now + KEY_LIFETIME);
482 do_check_eq(cert.validUntil, now + CERT_LIFETIME);
483 exp = Number(payload.exp);
485 do_check_eq(exp, now + ASSERTION_LIFETIME);
486 _("----- DONE ----\n");
487 });
489 add_task(function test_resend_email_not_signed_in() {
490 let fxa = new MockFxAccounts();
492 try {
493 yield fxa.resendVerificationEmail();
494 } catch(err) {
495 do_check_eq(err.message,
496 "Cannot resend verification email; no signed-in user");
497 return;
498 }
499 do_throw("Should not be able to resend email when nobody is signed in");
500 });
502 add_test(function test_resend_email() {
503 let fxa = new MockFxAccounts();
504 let alice = getTestUser("alice");
506 let initialState = fxa.internal.currentAccountState;
508 // Alice is the user signing in; her email is unverified.
509 fxa.setSignedInUser(alice).then(() => {
510 log.debug("Alice signing in");
512 // We're polling for the first email
513 do_check_true(fxa.internal.currentAccountState !== initialState);
514 let aliceState = fxa.internal.currentAccountState;
516 // The polling timer is ticking
517 do_check_true(fxa.internal.currentTimer > 0);
519 fxa.internal.getUserAccountData().then(user => {
520 do_check_eq(user.email, alice.email);
521 do_check_eq(user.verified, false);
522 log.debug("Alice wants verification email resent");
524 fxa.resendVerificationEmail().then((result) => {
525 // Mock server response; ensures that the session token actually was
526 // passed to the client to make the hawk call
527 do_check_eq(result, "alice's session token");
529 // Timer was not restarted
530 do_check_true(fxa.internal.currentAccountState === aliceState);
532 // Timer is still ticking
533 do_check_true(fxa.internal.currentTimer > 0);
535 // Ok abort polling before we go on to the next test
536 fxa.internal.abortExistingFlow();
537 run_next_test();
538 });
539 });
540 });
541 });
543 add_test(function test_sign_out() {
544 do_test_pending();
545 let fxa = new MockFxAccounts();
546 let remoteSignOutCalled = false;
547 let client = fxa.internal.fxAccountsClient;
548 client.signOut = function() { remoteSignOutCalled = true; return Promise.resolve(); };
549 makeObserver(ONLOGOUT_NOTIFICATION, function() {
550 log.debug("test_sign_out_with_remote_error observed onlogout");
551 // user should be undefined after sign out
552 fxa.internal.getUserAccountData().then(user => {
553 do_check_eq(user, null);
554 do_check_true(remoteSignOutCalled);
555 do_test_finished();
556 run_next_test();
557 });
558 });
559 fxa.signOut();
560 });
562 add_test(function test_sign_out_with_remote_error() {
563 do_test_pending();
564 let fxa = new MockFxAccounts();
565 let client = fxa.internal.fxAccountsClient;
566 let remoteSignOutCalled = false;
567 // Force remote sign out to trigger an error
568 client.signOut = function() { remoteSignOutCalled = true; throw "Remote sign out error"; };
569 makeObserver(ONLOGOUT_NOTIFICATION, function() {
570 log.debug("test_sign_out_with_remote_error observed onlogout");
571 // user should be undefined after sign out
572 fxa.internal.getUserAccountData().then(user => {
573 do_check_eq(user, null);
574 do_check_true(remoteSignOutCalled);
575 do_test_finished();
576 run_next_test();
577 });
578 });
579 fxa.signOut();
580 });
582 /*
583 * End of tests.
584 * Utility functions follow.
585 */
587 function expandHex(two_hex) {
588 // Return a 64-character hex string, encoding 32 identical bytes.
589 let eight_hex = two_hex + two_hex + two_hex + two_hex;
590 let thirtytwo_hex = eight_hex + eight_hex + eight_hex + eight_hex;
591 return thirtytwo_hex + thirtytwo_hex;
592 };
594 function expandBytes(two_hex) {
595 return CommonUtils.hexToBytes(expandHex(two_hex));
596 };
598 function getTestUser(name) {
599 return {
600 email: name + "@example.com",
601 uid: "1ad7f502-4cc7-4ec1-a209-071fd2fae348",
602 sessionToken: name + "'s session token",
603 keyFetchToken: name + "'s keyfetch token",
604 unwrapBKey: expandHex("44"),
605 verified: false
606 };
607 }
609 function makeObserver(aObserveTopic, aObserveFunc) {
610 let observer = {
611 // nsISupports provides type management in C++
612 // nsIObserver is to be an observer
613 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
615 observe: function (aSubject, aTopic, aData) {
616 log.debug("observed " + aTopic + " " + aData);
617 if (aTopic == aObserveTopic) {
618 removeMe();
619 aObserveFunc(aSubject, aTopic, aData);
620 }
621 }
622 };
624 function removeMe() {
625 log.debug("removing observer for " + aObserveTopic);
626 Services.obs.removeObserver(observer, aObserveTopic);
627 }
629 Services.obs.addObserver(observer, aObserveTopic, false);
630 return removeMe;
631 }
633 function do_check_throws(func, result, stack)
634 {
635 if (!stack)
636 stack = Components.stack.caller;
638 try {
639 func();
640 } catch (ex) {
641 if (ex.name == result) {
642 return;
643 }
644 do_throw("Expected result " + result + ", caught " + ex, stack);
645 }
647 if (result) {
648 do_throw("Expected result " + result + ", none thrown", stack);
649 }
650 }