PageRenderTime 59ms CodeModel.GetById 2ms app.highlight 50ms RepoModel.GetById 1ms app.codeStats 0ms

/packages/accounts-password/password_tests.js

https://github.com/zszbys/meteor
JavaScript | 934 lines | 774 code | 71 blank | 89 comment | 7 complexity | 63a0c7ea1131b29c69cfb90d7769d33d MD5 | raw file
  1Accounts._noConnectionCloseDelayForTest = true;
  2
  3if (Meteor.isServer) {
  4  Meteor.methods({
  5    getUserId: function () {
  6      return this.userId;
  7    }
  8  });
  9}
 10
 11if (Meteor.isClient) (function () {
 12
 13  // XXX note, only one test can do login/logout things at once! for
 14  // now, that is this test.
 15
 16  Accounts._isolateLoginTokenForTest();
 17
 18  var logoutStep = function (test, expect) {
 19    Meteor.logout(expect(function (error) {
 20      test.equal(error, undefined);
 21      test.equal(Meteor.user(), null);
 22    }));
 23  };
 24  var loggedInAs = function (someUsername, test, expect) {
 25    return expect(function (error) {
 26      test.equal(error, undefined);
 27      test.equal(Meteor.user().username, someUsername);
 28    });
 29  };
 30  var waitForLoggedOutStep = function (test, expect) {
 31    pollUntil(expect, function () {
 32      return Meteor.userId() === null;
 33    }, 10 * 1000, 100);
 34  };
 35  var invalidateLoginsStep = function (test, expect) {
 36    Meteor.call("testInvalidateLogins", 'fail', expect(function (error) {
 37      test.isFalse(error);
 38    }));
 39  };
 40  var hideActualLoginErrorStep = function (test, expect) {
 41    Meteor.call("testInvalidateLogins", 'hide', expect(function (error) {
 42      test.isFalse(error);
 43    }));
 44  };
 45  var validateLoginsStep = function (test, expect) {
 46    Meteor.call("testInvalidateLogins", false, expect(function (error) {
 47      test.isFalse(error);
 48    }));
 49  };
 50
 51  testAsyncMulti("passwords - basic login with password", [
 52    function (test, expect) {
 53      // setup
 54      this.username = Random.id();
 55      this.email = Random.id() + '-intercept@example.com';
 56      this.password = 'password';
 57
 58      Accounts.createUser(
 59        {username: this.username, email: this.email, password: this.password},
 60        loggedInAs(this.username, test, expect));
 61    },
 62    function (test, expect) {
 63      test.notEqual(Meteor.userId(), null);
 64    },
 65    logoutStep,
 66    function (test, expect) {
 67      Meteor.loginWithPassword(this.username, this.password,
 68                               loggedInAs(this.username, test, expect));
 69    },
 70    logoutStep,
 71    // This next step tests reactive contexts which are reactive on
 72    // Meteor.user().
 73    function (test, expect) {
 74      // Set up a reactive context that only refreshes when Meteor.user() is
 75      // invalidated.
 76      var loaded = false;
 77      var handle = Deps.autorun(function () {
 78        if (Meteor.user() && Meteor.user().emails)
 79          loaded = true;
 80      });
 81      // At the beginning, we're not logged in.
 82      test.isFalse(loaded);
 83      Meteor.loginWithPassword(this.username, this.password, expect(function (error) {
 84        test.equal(error, undefined);
 85        test.notEqual(Meteor.userId(), null);
 86        // By the time of the login callback, the user should be loaded.
 87        test.isTrue(Meteor.user().emails);
 88        // Flushing should get us the rerun as well.
 89        Deps.flush();
 90        test.isTrue(loaded);
 91        handle.stop();
 92      }));
 93    },
 94    logoutStep,
 95    function (test, expect) {
 96      Meteor.loginWithPassword({username: this.username}, this.password,
 97                               loggedInAs(this.username, test, expect));
 98    },
 99    logoutStep,
100    function (test, expect) {
101      Meteor.loginWithPassword(this.email, this.password,
102                               loggedInAs(this.username, test, expect));
103    },
104    logoutStep,
105    function (test, expect) {
106      Meteor.loginWithPassword({email: this.email}, this.password,
107                               loggedInAs(this.username, test, expect));
108    },
109    logoutStep
110  ]);
111
112
113  testAsyncMulti("passwords - plain text passwords", [
114    function (test, expect) {
115      // setup
116      this.username = Random.id();
117      this.email = Random.id() + '-intercept@example.com';
118      this.password = 'password';
119
120      // create user with raw password (no API, need to invoke callLoginMethod
121      // directly)
122      Accounts.callLoginMethod({
123        methodName: 'createUser',
124        methodArguments: [{username: this.username, password: this.password}],
125        userCallback: loggedInAs(this.username, test, expect)
126      });
127    },
128    logoutStep,
129    // check can login normally with this password.
130    function(test, expect) {
131      Meteor.loginWithPassword({username: this.username}, this.password,
132                               loggedInAs(this.username, test, expect));
133    },
134    logoutStep,
135    // plain text password. no API for this, have to invoke callLoginMethod
136    // directly.
137    function (test, expect) {
138      Accounts.callLoginMethod({
139        // wrong password
140        methodArguments: [{user: {username: this.username}, password: 'wrong'}],
141        userCallback: expect(function (error) {
142          test.isTrue(error);
143          test.isFalse(Meteor.user());
144        })});
145    },
146    function (test, expect) {
147      Accounts.callLoginMethod({
148        // right password
149        methodArguments: [{user: {username: this.username},
150                           password: this.password}],
151        userCallback: loggedInAs(this.username, test, expect)
152      });
153    },
154    logoutStep
155  ]);
156
157
158  testAsyncMulti("passwords - changing passwords", [
159    function (test, expect) {
160      // setup
161      this.username = Random.id();
162      this.email = Random.id() + '-intercept@example.com';
163      this.password = 'password';
164      this.password2 = 'password2';
165
166      Accounts.createUser(
167        {username: this.username, email: this.email, password: this.password},
168        loggedInAs(this.username, test, expect));
169    },
170    // change password with bad old password. we stay logged in.
171    function (test, expect) {
172      var self = this;
173      Accounts.changePassword('wrong', 'doesntmatter', expect(function (error) {
174        test.isTrue(error);
175        test.equal(Meteor.user().username, self.username);
176      }));
177    },
178    // change password with good old password.
179    function (test, expect) {
180      Accounts.changePassword(this.password, this.password2,
181                              loggedInAs(this.username, test, expect));
182    },
183    logoutStep,
184    // old password, failed login
185    function (test, expect) {
186      Meteor.loginWithPassword(this.email, this.password, expect(function (error) {
187        test.isTrue(error);
188        test.isFalse(Meteor.user());
189      }));
190    },
191    // new password, success
192    function (test, expect) {
193      Meteor.loginWithPassword(this.email, this.password2,
194                               loggedInAs(this.username, test, expect));
195    },
196    logoutStep
197  ]);
198
199  testAsyncMulti("passwords - changing password logs out other clients", [
200    function (test, expect) {
201      this.username = Random.id();
202      this.email = Random.id() + '-intercept@example.com';
203      this.password = 'password';
204      this.password2 = 'password2';
205      Accounts.createUser(
206        { username: this.username, email: this.email, password: this.password },
207        loggedInAs(this.username, test, expect));
208    },
209    // Log in a second connection as this user.
210    function (test, expect) {
211      var self = this;
212
213      self.secondConn = DDP.connect(Meteor.absoluteUrl());
214      self.secondConn.call('login',
215                { user: { username: self.username }, password: self.password },
216                expect(function (err, result) {
217                  test.isFalse(err);
218                  self.secondConn.setUserId(result.id);
219                  test.isTrue(self.secondConn.userId());
220
221                  self.secondConn.onReconnect = function () {
222                    self.secondConn.apply(
223                      'login',
224                      [{ resume: result.token }],
225                      { wait: true },
226                      function (err, result) {
227                        self.secondConn.setUserId(result && result.id || null);
228                      }
229                    );
230                  };
231                }));
232    },
233    function (test, expect) {
234      var self = this;
235      Accounts.changePassword(self.password, self.password2, expect(function (err) {
236        test.isFalse(err);
237      }));
238    },
239    // Now that we've changed the password, wait until the second
240    // connection gets logged out.
241    function (test, expect) {
242      var self = this;
243      pollUntil(expect, function () {
244        return self.secondConn.userId() === null;
245      }, 10 * 1000, 100);
246    }
247  ]);
248
249
250  testAsyncMulti("passwords - new user hooks", [
251    function (test, expect) {
252      // setup
253      this.username = Random.id();
254      this.email = Random.id() + '-intercept@example.com';
255      this.password = 'password';
256    },
257    // test Accounts.validateNewUser
258    function(test, expect) {
259      Accounts.createUser(
260        {username: this.username, password: this.password,
261         // should fail the new user validators
262         profile: {invalid: true}},
263        expect(function (error) {
264          test.equal(error.error, 403);
265          test.equal(error.reason, "User validation failed");
266        }));
267    },
268    logoutStep,
269    function(test, expect) {
270      Accounts.createUser(
271        {username: this.username, password: this.password,
272         // should fail the new user validator with a special
273         // exception
274         profile: {invalidAndThrowException: true}},
275        expect(function (error) {
276          test.equal(
277            error.reason,
278            "An exception thrown within Accounts.validateNewUser");
279        }));
280    },
281    // test Accounts.onCreateUser
282    function(test, expect) {
283      Accounts.createUser(
284        {username: this.username, password: this.password,
285         testOnCreateUserHook: true},
286        loggedInAs(this.username, test, expect));
287    },
288    function(test, expect) {
289      test.equal(Meteor.user().profile.touchedByOnCreateUser, true);
290    },
291    logoutStep
292  ]);
293
294
295  testAsyncMulti("passwords - Meteor.user()", [
296    function (test, expect) {
297      // setup
298      this.username = Random.id();
299      this.password = 'password';
300
301      Accounts.createUser(
302        {username: this.username, password: this.password,
303         testOnCreateUserHook: true},
304        loggedInAs(this.username, test, expect));
305    },
306    // test Meteor.user(). This test properly belongs in
307    // accounts-base/accounts_tests.js, but this is where the tests that
308    // actually log in are.
309    function(test, expect) {
310      var self = this;
311      var clientUser = Meteor.user();
312      Accounts.connection.call('testMeteorUser', expect(function (err, result) {
313        test.equal(result._id, clientUser._id);
314        test.equal(result.username, clientUser.username);
315        test.equal(result.username, self.username);
316        test.equal(result.profile.touchedByOnCreateUser, true);
317        test.equal(err, undefined);
318      }));
319    },
320    function(test, expect) {
321      // Test that even with no published fields, we still have a document.
322      Accounts.connection.call('clearUsernameAndProfile', expect(function() {
323        test.isTrue(Meteor.userId());
324        var user = Meteor.user();
325        test.equal(user, {_id: Meteor.userId()});
326      }));
327    },
328    logoutStep,
329    function(test, expect) {
330      var clientUser = Meteor.user();
331      test.equal(clientUser, null);
332      test.equal(Meteor.userId(), null);
333      Accounts.connection.call('testMeteorUser', expect(function (err, result) {
334        test.equal(err, undefined);
335        test.equal(result, null);
336      }));
337    }
338  ]);
339
340  testAsyncMulti("passwords - allow rules", [
341    // create a second user to have an id for in a later test
342    function (test, expect) {
343      this.otherUsername = Random.id();
344      Accounts.createUser(
345        {username: this.otherUsername, password: 'dontcare',
346         testOnCreateUserHook: true},
347        loggedInAs(this.otherUsername, test, expect));
348    },
349    function (test, expect) {
350      this.otherUserId = Meteor.userId();
351    },
352    function (test, expect) {
353      // real setup
354      this.username = Random.id();
355      this.password = 'password';
356
357      Accounts.createUser(
358        {username: this.username, password: this.password,
359         testOnCreateUserHook: true},
360        loggedInAs(this.username, test, expect));
361    },
362    // test the default Meteor.users allow rule. This test properly belongs in
363    // accounts-base/accounts_tests.js, but this is where the tests that
364    // actually log in are.
365    function(test, expect) {
366      this.userId = Meteor.userId();
367      test.notEqual(this.userId, null);
368      test.notEqual(this.userId, this.otherUserId);
369      // Can't update fields other than profile.
370      Meteor.users.update(
371        this.userId, {$set: {disallowed: true, 'profile.updated': 42}},
372        expect(function (err) {
373          test.isTrue(err);
374          test.equal(err.error, 403);
375          test.isFalse(_.has(Meteor.user(), 'disallowed'));
376          test.isFalse(_.has(Meteor.user().profile, 'updated'));
377        }));
378    },
379    function(test, expect) {
380      // Can't update another user.
381      Meteor.users.update(
382        this.otherUserId, {$set: {'profile.updated': 42}},
383        expect(function (err) {
384          test.isTrue(err);
385          test.equal(err.error, 403);
386        }));
387    },
388    function(test, expect) {
389      // Can't update using a non-ID selector. (This one is thrown client-side.)
390      test.throws(function () {
391        Meteor.users.update(
392          {username: this.username}, {$set: {'profile.updated': 42}});
393      });
394      test.isFalse(_.has(Meteor.user().profile, 'updated'));
395    },
396    function(test, expect) {
397      // Can update own profile using ID.
398      Meteor.users.update(
399        this.userId, {$set: {'profile.updated': 42}},
400        expect(function (err) {
401          test.isFalse(err);
402          test.equal(42, Meteor.user().profile.updated);
403        }));
404    },
405    logoutStep
406  ]);
407
408
409  testAsyncMulti("passwords - tokens", [
410    function (test, expect) {
411      // setup
412      this.username = Random.id();
413      this.password = 'password';
414
415      Accounts.createUser(
416        {username: this.username, password: this.password},
417        loggedInAs(this.username, test, expect));
418    },
419
420    function (test, expect) {
421      // we can't login with an invalid token
422      var expectLoginError = expect(function (err) {
423        test.isTrue(err);
424      });
425      Meteor.loginWithToken('invalid', expectLoginError);
426    },
427
428    function (test, expect) {
429      // we can login with a valid token
430      var expectLoginOK = expect(function (err) {
431        test.isFalse(err);
432      });
433      Meteor.loginWithToken(Accounts._storedLoginToken(), expectLoginOK);
434    },
435
436    function (test, expect) {
437      // test logging out invalidates our token
438      var expectLoginError = expect(function (err) {
439        test.isTrue(err);
440      });
441      var token = Accounts._storedLoginToken();
442      test.isTrue(token);
443      Meteor.logout(function () {
444        Meteor.loginWithToken(token, expectLoginError);
445      });
446    },
447
448    function (test, expect) {
449      var self = this;
450      // Test that login tokens get expired. We should get logged out when a
451      // token expires, and not be able to log in again with the same token.
452      var expectNoError = expect(function (err) {
453        test.isFalse(err);
454      });
455
456      Meteor.loginWithPassword(this.username, this.password, function (error) {
457        self.token = Accounts._storedLoginToken();
458        test.isTrue(self.token);
459        expectNoError(error);
460        Accounts.connection.call("expireTokens");
461      });
462    },
463    waitForLoggedOutStep,
464    function (test, expect) {
465      var token = Accounts._storedLoginToken();
466      test.isFalse(token);
467    },
468    function (test, expect) {
469      // Test that once expireTokens is finished, we can't login again with our
470      // previous token.
471      Meteor.loginWithToken(this.token, expect(function (err, result) {
472        test.isTrue(err);
473        test.equal(Meteor.userId(), null);
474      }));
475    },
476    logoutStep,
477    function (test, expect) {
478      var self = this;
479      // Test that Meteor.logoutOtherClients logs out a second
480      // authentcated connection while leaving Accounts.connection
481      // logged in.
482      var secondConn = DDP.connect(Meteor.absoluteUrl());
483      var token;
484
485      var expectSecondConnLoggedOut = expect(function (err, result) {
486        test.isTrue(err);
487      });
488
489      var expectAccountsConnLoggedIn = expect(function (err, result) {
490        test.isFalse(err);
491      });
492
493      var expectSecondConnLoggedIn = expect(function (err, result) {
494        test.equal(result.token, token);
495        test.isFalse(err);
496        Meteor.logoutOtherClients(function (err) {
497          test.isFalse(err);
498          secondConn.call('login', { resume: token },
499                          expectSecondConnLoggedOut);
500          Accounts.connection.call('login', {
501            resume: Accounts._storedLoginToken()
502          }, expectAccountsConnLoggedIn);
503        });
504      });
505
506      Meteor.loginWithPassword(
507        self.username,
508        self.password,
509        expect(function (err) {
510          test.isFalse(err);
511          token = Accounts._storedLoginToken();
512          test.isTrue(token);
513          secondConn.call('login', { resume: token },
514                          expectSecondConnLoggedIn);
515        })
516      );
517    },
518    logoutStep,
519
520    // The tests below this point are for the deprecated
521    // `logoutOtherClients` method.
522
523    function (test, expect) {
524      var self = this;
525
526      // Test that Meteor.logoutOtherClients logs out a second authenticated
527      // connection while leaving Accounts.connection logged in.
528      var token;
529      self.secondConn = DDP.connect(Meteor.absoluteUrl());
530
531      var expectLoginError = expect(function (err) {
532        test.isTrue(err);
533      });
534      var expectValidToken = expect(function (err, result) {
535        test.isFalse(err);
536        test.isTrue(result);
537        self.tokenFromLogoutOthers = result.token;
538      });
539      var expectSecondConnLoggedIn = expect(function (err, result) {
540        test.equal(result.token, token);
541        test.isFalse(err);
542        // This test will fail if an unrelated reconnect triggers before the
543        // connection is logged out. In general our tests aren't resilient to
544        // mid-test reconnects.
545        self.secondConn.onReconnect = function () {
546          self.secondConn.call("login", { resume: token }, expectLoginError);
547        };
548        Accounts.connection.call("logoutOtherClients", expectValidToken);
549      });
550
551      Meteor.loginWithPassword(this.username, this.password, expect(function (err) {
552        test.isFalse(err);
553        token = Accounts._storedLoginToken();
554        self.beforeLogoutOthersToken = token;
555        test.isTrue(token);
556        self.secondConn.call("login", { resume: token },
557                             expectSecondConnLoggedIn);
558      }));
559    },
560    // Test that logoutOtherClients logged out Accounts.connection and that the
561    // previous token is no longer valid.
562    waitForLoggedOutStep,
563    function (test, expect) {
564      var self = this;
565      var token = Accounts._storedLoginToken();
566      test.isFalse(token);
567      this.secondConn.close();
568      Meteor.loginWithToken(
569        self.beforeLogoutOthersToken,
570        expect(function (err) {
571          test.isTrue(err);
572          test.isFalse(Meteor.userId());
573        })
574      );
575    },
576    // Test that logoutOtherClients returned a new token that we can use to
577    // log in.
578    function (test, expect) {
579      var self = this;
580      Meteor.loginWithToken(
581        self.tokenFromLogoutOthers,
582        expect(function (err) {
583          test.isFalse(err);
584          test.isTrue(Meteor.userId());
585        })
586      );
587    },
588    logoutStep,
589
590
591
592    function (test, expect) {
593      var self = this;
594      // Test that deleting a user logs out that user's connections.
595      Meteor.loginWithPassword(this.username, this.password, expect(function (err) {
596        test.isFalse(err);
597        Accounts.connection.call("removeUser", self.username);
598      }));
599    },
600    waitForLoggedOutStep
601  ]);
602
603  testAsyncMulti("passwords - validateLoginAttempt", [
604    function (test, expect) {
605      this.username = Random.id();
606      this.password = "password";
607
608      Accounts.createUser(
609        {username: this.username, password: this.password},
610        loggedInAs(this.username, test, expect));
611    },
612    logoutStep,
613    invalidateLoginsStep,
614    function (test, expect) {
615      Meteor.loginWithPassword(
616        this.username,
617        this.password,
618        expect(function (error) {
619          test.isTrue(error);
620          test.equal(error.reason, "Login forbidden");
621        })
622      );
623    },
624    validateLoginsStep,
625    function (test, expect) {
626      Meteor.loginWithPassword(
627        "no such user",
628        "some password",
629        expect(function (error) {
630          test.isTrue(error);
631          test.equal(error.reason, 'User not found');
632        })
633      );
634    },
635    hideActualLoginErrorStep,
636    function (test, expect) {
637      Meteor.loginWithPassword(
638        "no such user",
639        "some password",
640        expect(function (error) {
641          test.isTrue(error);
642          test.equal(error.reason, 'hide actual error');
643        })
644      );
645    },
646    validateLoginsStep
647  ]);
648
649  testAsyncMulti("passwords - onLogin hook", [
650    function (test, expect) {
651      Meteor.call("testCaptureLogins", expect(function (error) {
652        test.isFalse(error);
653      }));
654    },
655    function (test, expect) {
656      this.username = Random.id();
657      this.password = "password";
658
659      Accounts.createUser(
660        {username: this.username, password: this.password},
661        loggedInAs(this.username, test, expect));
662    },
663    function (test, expect) {
664      var self = this;
665      Meteor.call("testFetchCapturedLogins", expect(function (error, logins) {
666        test.isFalse(error);
667        test.equal(logins.length, 1);
668        var login = logins[0];
669        test.isTrue(login.successful);
670        var attempt = login.attempt;
671        test.equal(attempt.type, "password");
672        test.isTrue(attempt.allowed);
673        test.equal(attempt.methodName, "createUser");
674        test.equal(attempt.methodArguments[0].username, self.username);
675      }));
676    }
677  ]);
678
679  testAsyncMulti("passwords - onLoginFailed hook", [
680    function (test, expect) {
681      this.username = Random.id();
682      this.password = "password";
683
684      Accounts.createUser(
685        {username: this.username, password: this.password},
686        loggedInAs(this.username, test, expect));
687    },
688    logoutStep,
689    function (test, expect) {
690      Meteor.call("testCaptureLogins", expect(function (error) {
691        test.isFalse(error);
692      }));
693    },
694    function (test, expect) {
695      Meteor.loginWithPassword(this.username, "incorrect", expect(function (error) {
696        test.isTrue(error);
697      }));
698    },
699    function (test, expect) {
700      Meteor.call("testFetchCapturedLogins", expect(function (error, logins) {
701        test.isFalse(error);
702        test.equal(logins.length, 1);
703        var login = logins[0];
704        test.isFalse(login.successful);
705        var attempt = login.attempt;
706        test.equal(attempt.type, "password");
707        test.isFalse(attempt.allowed);
708        test.equal(attempt.error.reason, "Incorrect password");
709      }));
710    },
711    function (test, expect) {
712      Meteor.call("testCaptureLogins", expect(function (error) {
713        test.isFalse(error);
714      }));
715    },
716    function (test, expect) {
717      Meteor.loginWithPassword("no such user", "incorrect", expect(function (error) {
718        test.isTrue(error);
719      }));
720    },
721    function (test, expect) {
722      Meteor.call("testFetchCapturedLogins", expect(function (error, logins) {
723        test.isFalse(error);
724        test.equal(logins.length, 1);
725        var login = logins[0];
726        test.isFalse(login.successful);
727        var attempt = login.attempt;
728        test.equal(attempt.type, "password");
729        test.isFalse(attempt.allowed);
730        test.equal(attempt.error.reason, "User not found");
731      }));
732    }
733  ]);
734
735  testAsyncMulti("passwords - srp to bcrypt upgrade", [
736    logoutStep,
737    // Create user with old SRP credentials in the database.
738    function (test, expect) {
739      var self = this;
740      Meteor.call("testCreateSRPUser", expect(function (error, result) {
741        test.isFalse(error);
742        self.username = result;
743      }));
744    },
745    // We are able to login with the old style credentials in the database.
746    function (test, expect) {
747      Meteor.loginWithPassword(this.username, 'abcdef', expect(function (error) {
748        test.isFalse(error);
749      }));
750    },
751    function (test, expect) {
752      Meteor.call("testSRPUpgrade", this.username, expect(function (error) {
753        test.isFalse(error);
754      }));
755    },
756    logoutStep,
757    // After the upgrade to bcrypt we're still able to login.
758    function (test, expect) {
759      Meteor.loginWithPassword(this.username, 'abcdef', expect(function (error) {
760        test.isFalse(error);
761      }));
762    },
763    logoutStep,
764    function (test, expect) {
765      Meteor.call("removeUser", this.username, expect(function (error) {
766        test.isFalse(error);
767      }));
768    }
769  ]);
770
771  testAsyncMulti("passwords - srp to bcrypt upgrade via password change", [
772    logoutStep,
773    // Create user with old SRP credentials in the database.
774    function (test, expect) {
775      var self = this;
776      Meteor.call("testCreateSRPUser", expect(function (error, result) {
777        test.isFalse(error);
778        self.username = result;
779      }));
780    },
781    // Log in with the plaintext password handler, which should NOT upgrade us to bcrypt.
782    function (test, expect) {
783      Accounts.callLoginMethod({
784        methodName: "login",
785        methodArguments: [ { user: { username: this.username }, password: "abcdef" } ],
786        userCallback: expect(function (err) {
787          test.isFalse(err);
788        })
789      });
790    },
791    function (test, expect) {
792      Meteor.call("testNoSRPUpgrade", this.username, expect(function (error) {
793        test.isFalse(error);
794      }));
795    },
796    // Changing our password should upgrade us to bcrypt.
797    function (test, expect) {
798      Accounts.changePassword("abcdef", "abcdefg", expect(function (error) {
799        test.isFalse(error);
800      }));
801    },
802    function (test, expect) {
803      Meteor.call("testSRPUpgrade", this.username, expect(function (error) {
804        test.isFalse(error);
805      }));
806    },
807    // And after the upgrade we should be able to change our password again.
808    function (test, expect) {
809      Accounts.changePassword("abcdefg", "abcdef", expect(function (error) {
810        test.isFalse(error);
811      }));
812    },
813    logoutStep
814  ]);
815}) ();
816
817
818if (Meteor.isServer) (function () {
819
820  Tinytest.add(
821    'passwords - setup more than one onCreateUserHook',
822    function (test) {
823      test.throws(function() {
824        Accounts.onCreateUser(function () {});
825      });
826    });
827
828
829  Tinytest.add(
830    'passwords - createUser hooks',
831    function (test) {
832      var username = Random.id();
833      test.throws(function () {
834        // should fail the new user validators
835        Accounts.createUser({username: username, profile: {invalid: true}});
836      });
837
838      var userId = Accounts.createUser({username: username,
839                                        testOnCreateUserHook: true});
840
841      test.isTrue(userId);
842      var user = Meteor.users.findOne(userId);
843      test.equal(user.profile.touchedByOnCreateUser, true);
844    });
845
846
847  Tinytest.add(
848    'passwords - setPassword',
849    function (test) {
850      var username = Random.id();
851
852      var userId = Accounts.createUser({username: username});
853
854      var user = Meteor.users.findOne(userId);
855      // no services yet.
856      test.equal(user.services.password, undefined);
857
858      // set a new password.
859      Accounts.setPassword(userId, 'new password');
860      user = Meteor.users.findOne(userId);
861      var oldSaltedHash = user.services.password.bcrypt;
862      test.isTrue(oldSaltedHash);
863
864      // reset with the same password, see we get a different salted hash
865      Accounts.setPassword(userId, 'new password');
866      user = Meteor.users.findOne(userId);
867      var newSaltedHash = user.services.password.bcrypt;
868      test.isTrue(newSaltedHash);
869      test.notEqual(oldSaltedHash, newSaltedHash);
870
871      // cleanup
872      Meteor.users.remove(userId);
873    });
874
875
876  // This test properly belongs in accounts-base/accounts_tests.js, but
877  // this is where the tests that actually log in are.
878  Tinytest.add('accounts - user() out of context', function (test) {
879    // basic server context, no method.
880    test.throws(function () {
881      Meteor.user();
882    });
883  });
884
885  // XXX would be nice to test Accounts.config({forbidClientAccountCreation: true})
886
887  Tinytest.addAsync(
888    'passwords - login token observes get cleaned up',
889    function (test, onComplete) {
890      var username = Random.id();
891      Accounts.createUser({
892        username: username,
893        password: 'password'
894      });
895
896      makeTestConnection(
897        test,
898        function (clientConn, serverConn) {
899          serverConn.onClose(function () {
900            test.isFalse(Accounts._getUserObserve(serverConn.id));
901            onComplete();
902          });
903          var result = clientConn.call('login', {
904            user: {username: username},
905            password: 'password'
906          });
907          test.isTrue(result);
908          var token = Accounts._getAccountData(serverConn.id, 'loginToken');
909          test.isTrue(token);
910
911          // We poll here, instead of just checking `_getUserObserve`
912          // once, because the login method defers the creation of the
913          // observe, and setting up the observe yields, so we could end
914          // up here before the observe has been set up.
915          simplePoll(
916            function () {
917              return !! Accounts._getUserObserve(serverConn.id);
918            },
919            function () {
920              test.isTrue(Accounts._getUserObserve(serverConn.id));
921              clientConn.disconnect();
922            },
923            function () {
924              test.fail("timed out waiting for user observe for connection " +
925                        serverConn.id);
926              onComplete();
927            }
928          );
929        },
930        onComplete
931      );
932    }
933  );
934}) ();