Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Change interface for determining if the user doc is loading to a new …

…reactive

function Meteor.userLoading(). The value of Meteor.user() is not defined if
userLoading returns true.

The Handlebars helper currentUser is "true" if the user is loading, and a new
helper currentUserLoading is equivalent to Meteor.userLoading.

The current user subscription is now named meteor.currentUser rather than being
an unnamed sub. (loginServiceConfiguration is renamed
meteor.loginServiceConfiguration to match.) This subscription is sub'd from when
you log in and unsub'd from when you log out (or if you log in with different
credentials).

I was very careful to make sure that in the case of "sub #1, unsub #1, sub #2,
sub #1 is ready" we do not declare the user to be ready. I could have instead
modified livedata_connection to not call ready callbacks for unsub'd
subscriptions (add a "delete self.sub_ready_callbacks[obj._id]" to the self.subs
removed function) but this seemed less invasive.

The password and email tests use this to take a more rigorous approach to
waiting for the data to load, and they change the localStorage keys so that
multiple tabs running tests don't interact via localStorage.
  • Loading branch information...
commit 03c91d4dc66af4488dddd233b7e3dc2eb551b558 1 parent 7a5d6b7
@glasser glasser authored
View
28 packages/accounts-base/accounts_client.js
@@ -4,21 +4,13 @@
return Meteor.default_connection.userId();
};
+ // Only call this in a context where you've already checked
+ // Meteor.userLoading().
Meteor.user = function () {
var userId = Meteor.userId();
- if (userId) {
- var result = Meteor.users.findOne(userId);
- if (result) {
- return result;
- } else {
- // If the login method completes but new subcriptions haven't
- // yet been sent down to the client, this is the best we can
- // do
- return {_id: userId, loading: true};
- }
- } else {
+ if (!userId)
return null;
- }
+ return Meteor.users.findOne(userId);
};
Meteor.logout = function (callback) {
@@ -32,19 +24,25 @@
});
};
- // If we're using Handlebars, register the {{currentUser}} global
- // helper
+ // If we're using Handlebars, register the {{currentUser}} and
+ // {{currentUserLoading}} global helpers.
if (window.Handlebars) {
Handlebars.registerHelper('currentUser', function () {
+ // This lets us do "{{#if currentUser}}" outside of "{{#if
+ // currentUserLoading}}".
+ if (Meteor.userLoading()) return true;
return Meteor.user();
});
+ Handlebars.registerHelper('currentUserLoading', function () {
+ return Meteor.userLoading();
+ });
}
// XXX this can be simplified if we merge in
// https://github.com/meteor/meteor/pull/273
var loginServicesConfigured = false;
var loginServicesConfiguredListeners = new Meteor.deps._ContextSet;
- Meteor.subscribe("loginServiceConfiguration", function () {
+ Meteor.subscribe("meteor.loginServiceConfiguration", function () {
loginServicesConfigured = true;
loginServicesConfiguredListeners.invalidateAll();
});
View
10 packages/accounts-base/accounts_server.js
@@ -233,13 +233,15 @@
/// PUBLISHING DATA
///
- // Always publish the current user's record to the client.
- Meteor.publish(null, function() {
+ // Publish the current user's record to the client.
+ Meteor.publish("meteor.currentUser", function() {
if (this.userId())
return Meteor.users.find({_id: this.userId()},
{fields: {profile: 1, username: 1, emails: 1}});
- else
+ else {
+ this.complete();
return null;
+ }
}, {is_auto: true});
// If autopublish is on, also publish everyone else's user record.
@@ -252,7 +254,7 @@
});
// Publish all login service configuration fields other than secret.
- Meteor.publish("loginServiceConfiguration", function () {
+ Meteor.publish("meteor.loginServiceConfiguration", function () {
return Accounts.configuration.find({}, {fields: {secret: 0}});
}, {is_auto: true}); // not techincally autopublish, but stops the warning.
View
35 packages/accounts-base/localstorage_token.js
@@ -3,6 +3,14 @@
var loginTokenKey = "Meteor.loginToken";
var userIdKey = "Meteor.userId";
+ // Call this from the top level of the test file for any test that does
+ // logging in and out, to protect multiple tabs running the same tests
+ // simultaneously from interfering with each others' localStorage.
+ Accounts._isolateLoginTokenForTest = function () {
+ loginTokenKey = loginTokenKey + Meteor.uuid();
+ userIdKey = userIdKey + Meteor.uuid();
+ };
+
Accounts._storeLoginToken = function(userId, token) {
localStorage.setItem(userIdKey, userId);
localStorage.setItem(loginTokenKey, token);
@@ -29,10 +37,23 @@
return localStorage.getItem(userIdKey);
};
+ var userLoadingListeners = new Meteor.deps._ContextSet;
+ var currentUserSubscriptionData;
+
+ Meteor.userLoading = function () {
+ userLoadingListeners.addCurrentContext();
+ return currentUserSubscriptionData && currentUserSubscriptionData.loading;
+ };
+
Accounts._makeClientLoggedOut = function() {
Accounts._unstoreLoginToken();
Meteor.default_connection.setUserId(null);
Meteor.default_connection.onReconnect = null;
+ userLoadingListeners.invalidateAll();
+ if (currentUserSubscriptionData) {
+ currentUserSubscriptionData.handle.stop();
+ currentUserSubscriptionData = null;
+ }
};
Accounts._makeClientLoggedIn = function(userId, token) {
@@ -48,6 +69,20 @@
}
});
};
+ userLoadingListeners.invalidateAll();
+ if (currentUserSubscriptionData) {
+ currentUserSubscriptionData.handle.stop();
+ }
+ var data = currentUserSubscriptionData = {loading: true};
+ data.handle = Meteor.subscribe(
+ "meteor.currentUser", function () {
+ // Important! We use "data" here, not "currentUserSubscriptionData", so
+ // that if we log out and in again before this subscription is ready, we
+ // don't make currentUserSubscriptionData look ready just because this
+ // older iteration of subscribing is ready.
+ data.loading = false;
+ userLoadingListeners.invalidateAll();
+ });
};
})();
View
84 packages/accounts-password/email_tests.js
@@ -11,6 +11,8 @@
var validateEmailToken;
var enrollAccountToken;
+ Accounts._isolateLoginTokenForTest();
+
testAsyncMulti("accounts emails - reset password flow", [
function (test, expect) {
email1 = Meteor.uuid() + "-intercept@example.com";
@@ -65,6 +67,7 @@
var getValidateEmailToken = function (email, test, expect) {
Meteor.call("getInterceptedEmails", email, expect(function (error, result) {
+ test.isFalse(error);
test.notEqual(result, undefined);
test.equal(result.length, 1);
var content = result[0];
@@ -77,15 +80,28 @@
}));
};
+ var waitUntilLoggedIn = function (test, expect) {
+ var unblockNextFunction = expect();
+ var quiesceCallback = function () {
+ Meteor._autorun(function (handle) {
+ if (Meteor.userLoading()) return;
+ handle.stop();
+ unblockNextFunction();
+ });
+ };
+ return expect(function (error) {
+ test.equal(error, undefined);
+ Meteor.default_connection.onQuiesce(quiesceCallback);
+ });
+ };
+
testAsyncMulti("accounts emails - validate email flow", [
function (test, expect) {
email2 = Meteor.uuid() + "-intercept@example.com";
email3 = Meteor.uuid() + "-intercept@example.com";
Accounts.createUser(
{email: email2, password: 'foobar'},
- expect(function (error) {
- test.equal(error, undefined);
- }));
+ waitUntilLoggedIn(test, expect));
},
function (test, expect) {
test.equal(Meteor.user().emails.length, 1);
@@ -96,17 +112,23 @@
getValidateEmailToken(email2, test, expect);
},
function (test, expect) {
- Accounts.validateEmail(validateEmailToken, expect(function(error) {
- test.isFalse(error);
- }));
- // ARGH! ON QUIESCE!!
- Meteor.default_connection.onQuiesce(expect(function () {
- test.equal(Meteor.user().emails.length, 1);
- test.equal(Meteor.user().emails[0].address, email2);
- test.isTrue(Meteor.user().emails[0].validated);
+ // Log out, to test that validateEmail logs us back in. (And if we don't
+ // do that, waitUntilLoggedIn won't be able to prevent race conditions.)
+ Meteor.logout(expect(function (error) {
+ test.equal(error, undefined);
+ test.equal(Meteor.user(), null);
}));
},
function (test, expect) {
+ Accounts.validateEmail(validateEmailToken,
+ waitUntilLoggedIn(test, expect));
+ },
+ function (test, expect) {
+ test.equal(Meteor.user().emails.length, 1);
+ test.equal(Meteor.user().emails[0].address, email2);
+ test.isTrue(Meteor.user().emails[0].validated);
+ },
+ function (test, expect) {
Meteor.call(
"addEmailForTestAndValidate", email3,
expect(function (error, result) {
@@ -124,15 +146,20 @@
getValidateEmailToken(email3, test, expect);
},
function (test, expect) {
- Accounts.validateEmail(validateEmailToken, expect(function(error) {
- test.isFalse(error);
+ // Log out, to test that validateEmail logs us back in. (And if we don't
+ // do that, waitUntilLoggedIn won't be able to prevent race conditions.)
+ Meteor.logout(expect(function (error) {
+ test.equal(error, undefined);
+ test.equal(Meteor.user(), null);
}));
},
function (test, expect) {
- Meteor.default_connection.onQuiesce(expect(function () {
- test.equal(Meteor.user().emails[1].address, email3);
- test.isTrue(Meteor.user().emails[1].validated);
- }));
+ Accounts.validateEmail(validateEmailToken,
+ waitUntilLoggedIn(test, expect));
+ },
+ function (test, expect) {
+ test.equal(Meteor.user().emails[1].address, email3);
+ test.isTrue(Meteor.user().emails[1].validated);
},
function (test, expect) {
Meteor.logout(expect(function (error) {
@@ -172,16 +199,13 @@
getEnrollAccountToken(email4, test, expect);
},
function (test, expect) {
- Accounts.resetPassword(enrollAccountToken, 'password', expect(function(error) {
- test.isFalse(error);
- }));
+ Accounts.resetPassword(enrollAccountToken, 'password',
+ waitUntilLoggedIn(test, expect));
},
function (test, expect) {
- Meteor.default_connection.onQuiesce(expect(function () {
- test.equal(Meteor.user().emails.length, 1);
- test.equal(Meteor.user().emails[0].address, email4);
- test.isTrue(Meteor.user().emails[0].validated);
- }));
+ test.equal(Meteor.user().emails.length, 1);
+ test.equal(Meteor.user().emails[0].address, email4);
+ test.isTrue(Meteor.user().emails[0].validated);
},
function (test, expect) {
Meteor.logout(expect(function (error) {
@@ -190,9 +214,13 @@
}));
},
function (test, expect) {
- Meteor.loginWithPassword({email: email4}, 'password', expect(function(error) {
- test.isFalse(error);
- }));
+ Meteor.loginWithPassword({email: email4}, 'password',
+ waitUntilLoggedIn(test ,expect));
+ },
+ function (test, expect) {
+ test.equal(Meteor.user().emails.length, 1);
+ test.equal(Meteor.user().emails[0].address, email4);
+ test.isTrue(Meteor.user().emails[0].validated);
},
function (test, expect) {
Meteor.logout(expect(function (error) {
View
98 packages/accounts-password/passwords_tests.js
@@ -3,6 +3,7 @@ if (Meteor.isClient) (function () {
// XXX note, only one test can do login/logout things at once! for
// now, that is this test.
+ Accounts._isolateLoginTokenForTest();
var logoutStep = function (test, expect) {
Meteor.logout(expect(function (error) {
@@ -11,6 +12,25 @@ if (Meteor.isClient) (function () {
}));
};
+ var verifyUsername = function (someUsername, test, expect) {
+ var callWhenLoaded = expect(function() {
+ test.equal(Meteor.user().username, someUsername);
+ });
+ return function () {
+ Meteor._autorun(function(handle) {
+ if (Meteor.userLoading()) return;
+ handle.stop();
+ callWhenLoaded();
+ });
+ };
+ };
+ var loggedInAs = function (someUsername, test, expect) {
+ var quiesceCallback = verifyUsername(someUsername, test, expect);
+ return expect(function (error) {
+ test.equal(error, undefined);
+ Meteor.default_connection.onQuiesce(quiesceCallback);
+ });
+ };
// declare variable outside the testAsyncMulti, so we can refer to
// them from multiple tests, but initialize them to new values inside
@@ -33,46 +53,29 @@ if (Meteor.isClient) (function () {
},
function (test, expect) {
- // XXX argh quiescence + tests === wtf. and i have no idea why
- // this was necessary here and not in other places. probably
- // because it's dependant on how long method call chains are in
- // other tests
- var quiesceCallback = expect(function () {
- test.equal(Meteor.user().username, username);
- });
- Accounts.createUser({username: username, email: email, password: password},
- expect(function (error) {
- test.equal(error, undefined);
- Meteor.default_connection.onQuiesce(quiesceCallback);
- }));
+ Accounts.createUser(
+ {username: username, email: email, password: password},
+ loggedInAs(username, test, expect));
},
logoutStep,
function (test, expect) {
- Meteor.loginWithPassword(username, password, expect(function (error) {
- test.equal(error, undefined);
- test.equal(Meteor.user().username, username);
- }));
+ Meteor.loginWithPassword(username, password,
+ loggedInAs(username, test, expect));
},
logoutStep,
function (test, expect) {
- Meteor.loginWithPassword({username: username}, password, expect(function (error) {
- test.equal(error, undefined);
- test.equal(Meteor.user().username, username);
- }));
+ Meteor.loginWithPassword({username: username}, password,
+ loggedInAs(username, test, expect));
},
logoutStep,
function (test, expect) {
- Meteor.loginWithPassword(email, password, expect(function (error) {
- test.equal(error, undefined);
- test.equal(Meteor.user().username, username);
- }));
+ Meteor.loginWithPassword(email, password,
+ loggedInAs(username, test, expect));
},
logoutStep,
function (test, expect) {
- Meteor.loginWithPassword({email: email}, password, expect(function (error) {
- test.equal(error, undefined);
- test.equal(Meteor.user().username, username);
- }));
+ Meteor.loginWithPassword({email: email}, password,
+ loggedInAs(username, test, expect));
},
logoutStep,
// plain text password. no API for this, have to send a raw message.
@@ -87,6 +90,7 @@ if (Meteor.isClient) (function () {
}));
},
function (test, expect) {
+ var quiesceCallback = verifyUsername(username, test, expect);
Meteor.call(
// right password
'login', {user: {email: email}, password: password},
@@ -96,22 +100,21 @@ if (Meteor.isClient) (function () {
test.isTrue(result.token);
// emulate the real login behavior, so as not to confuse test.
Accounts._makeClientLoggedIn(result.id, result.token);
- test.equal(Meteor.user().username, username);
+ Meteor.default_connection.onQuiesce(quiesceCallback);
}));
},
- // change password with bad old password.
+ // change password with bad old password. we stay logged in.
function (test, expect) {
+ var quiesceCallback = verifyUsername(username, test, expect);
Accounts.changePassword(password2, password2, expect(function (error) {
test.isTrue(error);
- test.equal(Meteor.user().username, username);
+ Meteor.default_connection.onQuiesce(quiesceCallback);
}));
},
// change password with good old password.
function (test, expect) {
- Accounts.changePassword(password, password2, expect(function (error) {
- test.equal(error, undefined);
- test.equal(Meteor.user().username, username);
- }));
+ Accounts.changePassword(password, password2,
+ loggedInAs(username, test, expect));
},
logoutStep,
// old password, failed login
@@ -123,14 +126,13 @@ if (Meteor.isClient) (function () {
},
// new password, success
function (test, expect) {
- Meteor.loginWithPassword(email, password2, expect(function (error) {
- test.equal(error, undefined);
- test.equal(Meteor.user().username, username);
- }));
+ Meteor.loginWithPassword(email, password2,
+ loggedInAs(username, test, expect));
},
logoutStep,
// create user with raw password
function (test, expect) {
+ var quiesceCallback = verifyUsername(username2, test, expect);
Meteor.call('createUser', {username: username2, password: password2},
expect(function (error, result) {
test.equal(error, undefined);
@@ -138,16 +140,13 @@ if (Meteor.isClient) (function () {
test.isTrue(result.token);
// emulate the real login behavior, so as not to confuse test.
Accounts._makeClientLoggedIn(result.id, result.token);
- test.equal(Meteor.user().username, username2);
+ Meteor.default_connection.onQuiesce(quiesceCallback);
}));
},
logoutStep,
function(test, expect) {
Meteor.loginWithPassword({username: username2}, password2,
- expect(function (error) {
- test.equal(error, undefined);
- test.equal(Meteor.user().username, username2);
- }));
+ loggedInAs(username2, test, expect));
},
logoutStep,
// test Accounts.validateNewUser
@@ -160,10 +159,13 @@ if (Meteor.isClient) (function () {
},
// test Accounts.onCreateUser
function(test, expect) {
- Accounts.createUser({username: username3, password: password3},
- {testOnCreateUserHook: true}, expect(function () {
- test.equal(Meteor.user().profile.touchedByOnCreateUser, true);
- }));
+ Accounts.createUser(
+ {username: username3, password: password3},
+ {testOnCreateUserHook: true},
+ loggedInAs(username3, test, expect));
+ },
+ function(test, expect) {
+ test.equal(Meteor.user().profile.touchedByOnCreateUser, true);
},
// test Meteor.user(). This test properly belongs in
View
44 packages/accounts-ui-unstyled/login_buttons.html
@@ -24,37 +24,37 @@
<template name="loginButtonsLoggedInDropdown">
<div class="login-link-and-dropdown-list">
- {{#if currentUser.loading}}
- <div class="loading"></div>
+ {{#if currentUserLoading}}
+ <div class="loading"></div>
{{else}}
- <a class="login-link-text" id="login-name-link">
- {{displayName}} ▾
- </a>
- {{#if dropdownVisible}}
- <div id="login-dropdown-list" class="accounts-dialog">
- <a class="login-close-text">Close</a>
- <div class="login-close-text-clear"></div>
-
- {{#if inChangePasswordFlow}}
- {{> loginButtonsChangePassword}}
- {{else}}
- {{#if allowChangingPassword}}
- <div class="login-button" id="login-buttons-open-change-password">Change password</div>
- {{/if}}
- <div class="login-button" id="login-buttons-logout">Logout</div>
+ <a class="login-link-text" id="login-name-link">
+ {{displayName}} ▾
+ </a>
+ {{#if dropdownVisible}}
+ <div id="login-dropdown-list" class="accounts-dialog">
+ <a class="login-close-text">Close</a>
+ <div class="login-close-text-clear"></div>
+
+ {{#if inChangePasswordFlow}}
+ {{> loginButtonsChangePassword}}
+ {{else}}
+ {{#if allowChangingPassword}}
+ <div class="login-button" id="login-buttons-open-change-password">Change password</div>
+ {{/if}}
+ <div class="login-button" id="login-buttons-logout">Logout</div>
+ {{/if}}
+ </div>
{{/if}}
- </div>
- {{/if}}
{{/if}}
</div>
</template>
<template name="loginButtonsLoggedInRow">
<div class="login-header">
- {{#if currentUser.loading}}
- <div class="loading"></div>
+ {{#if currentUserLoading}}
+ <div class="loading"></div>
{{else}}
- {{displayName}}
+ {{displayName}}
{{/if}}
</div>
<div class="login-button" id="login-buttons-logout">Logout</div>
Please sign in to comment.
Something went wrong with that request. Please try again.