diff --git a/docs/client/data.js b/docs/client/data.js
index ad1abab816..8afefc01ee 100644
--- a/docs/client/data.js
+++ b/docs/client/data.js
@@ -9,10 +9,52 @@ DocsData = {
"scope": "global",
"summary": "The namespace for all server-side accounts-related methods."
},
+ "Accounts.addEmail": {
+ "filepath": "accounts-password/password_server.js",
+ "kind": "function",
+ "lineno": 847,
+ "locus": "Server",
+ "longname": "Accounts.addEmail",
+ "memberof": "Accounts",
+ "name": "addEmail",
+ "options": [],
+ "params": [
+ {
+ "description": "
The ID of the user to update.
",
+ "name": "userId",
+ "type": {
+ "names": [
+ "String"
+ ]
+ }
+ },
+ {
+ "description": "A new email address for the user.
",
+ "name": "newEmail",
+ "type": {
+ "names": [
+ "String"
+ ]
+ }
+ },
+ {
+ "description": "Optional - whether the new email address should\nbe marked as verified. Defaults to false.
",
+ "name": "verified",
+ "optional": true,
+ "type": {
+ "names": [
+ "Boolean"
+ ]
+ }
+ }
+ ],
+ "scope": "static",
+ "summary": "Add an email address for a user. Use this instead of directly\nupdating the database. The operation will fail if there is a different user\nwith an email only differing in case. If the specified user has an existing\nemail only differing in case however, we replace it."
+ },
"Accounts.changePassword": {
"filepath": "accounts-password/password_client.js",
"kind": "function",
- "lineno": 150,
+ "lineno": 148,
"locus": "Client",
"longname": "Accounts.changePassword",
"memberof": "Accounts",
@@ -131,10 +173,76 @@ DocsData = {
"scope": "static",
"summary": "Options to customize emails sent from the Accounts system."
},
+ "Accounts.findUserByEmail": {
+ "filepath": "accounts-password/password_server.js",
+ "kind": "function",
+ "lineno": 138,
+ "locus": "Server",
+ "longname": "Accounts.findUserByEmail",
+ "memberof": "Accounts",
+ "name": "findUserByEmail",
+ "options": [],
+ "params": [
+ {
+ "description": "The email address to look for
",
+ "name": "email",
+ "type": {
+ "names": [
+ "String"
+ ]
+ }
+ }
+ ],
+ "returns": [
+ {
+ "description": "A user if found, else null
",
+ "type": {
+ "names": [
+ "Object"
+ ]
+ }
+ }
+ ],
+ "scope": "static",
+ "summary": "Finds the user with the specified email.\nFirst tries to match email case sensitively; if that fails, it\ntries case insensitively; but if more than one user matches the case\ninsensitive search, it returns null."
+ },
+ "Accounts.findUserByUsername": {
+ "filepath": "accounts-password/password_server.js",
+ "kind": "function",
+ "lineno": 123,
+ "locus": "Server",
+ "longname": "Accounts.findUserByUsername",
+ "memberof": "Accounts",
+ "name": "findUserByUsername",
+ "options": [],
+ "params": [
+ {
+ "description": "The username to look for
",
+ "name": "username",
+ "type": {
+ "names": [
+ "String"
+ ]
+ }
+ }
+ ],
+ "returns": [
+ {
+ "description": "A user if found, else null
",
+ "type": {
+ "names": [
+ "Object"
+ ]
+ }
+ }
+ ],
+ "scope": "static",
+ "summary": "Finds the user with the specified username.\nFirst tries to match username case sensitively; if that fails, it\ntries case insensitively; but if more than one user matches the case\ninsensitive search, it returns null."
+ },
"Accounts.forgotPassword": {
"filepath": "accounts-password/password_client.js",
"kind": "function",
- "lineno": 212,
+ "lineno": 210,
"locus": "Client",
"longname": "Accounts.forgotPassword",
"memberof": "Accounts",
@@ -173,10 +281,42 @@ DocsData = {
"scope": "static",
"summary": "Request a forgot password email."
},
+ "Accounts.removeEmail": {
+ "filepath": "accounts-password/password_server.js",
+ "kind": "function",
+ "lineno": 930,
+ "locus": "Server",
+ "longname": "Accounts.removeEmail",
+ "memberof": "Accounts",
+ "name": "removeEmail",
+ "options": [],
+ "params": [
+ {
+ "description": "The ID of the user to update.
",
+ "name": "userId",
+ "type": {
+ "names": [
+ "String"
+ ]
+ }
+ },
+ {
+ "description": "The email address to remove.
",
+ "name": "email",
+ "type": {
+ "names": [
+ "String"
+ ]
+ }
+ }
+ ],
+ "scope": "static",
+ "summary": "Remove an email address for a user. Use this instead of updating\nthe database directly."
+ },
"Accounts.resetPassword": {
"filepath": "accounts-password/password_client.js",
"kind": "function",
- "lineno": 232,
+ "lineno": 230,
"locus": "Client",
"longname": "Accounts.resetPassword",
"memberof": "Accounts",
@@ -218,7 +358,7 @@ DocsData = {
"Accounts.sendEnrollmentEmail": {
"filepath": "accounts-password/password_server.js",
"kind": "function",
- "lineno": 509,
+ "lineno": 584,
"locus": "Server",
"longname": "Accounts.sendEnrollmentEmail",
"memberof": "Accounts",
@@ -251,7 +391,7 @@ DocsData = {
"Accounts.sendResetPasswordEmail": {
"filepath": "accounts-password/password_server.js",
"kind": "function",
- "lineno": 444,
+ "lineno": 519,
"locus": "Server",
"longname": "Accounts.sendResetPasswordEmail",
"memberof": "Accounts",
@@ -284,7 +424,7 @@ DocsData = {
"Accounts.sendVerificationEmail": {
"filepath": "accounts-password/password_server.js",
"kind": "function",
- "lineno": 648,
+ "lineno": 723,
"locus": "Server",
"longname": "Accounts.sendVerificationEmail",
"memberof": "Accounts",
@@ -317,7 +457,7 @@ DocsData = {
"Accounts.setPassword": {
"filepath": "accounts-password/password_server.js",
"kind": "function",
- "lineno": 396,
+ "lineno": 471,
"locus": "Server",
"longname": "Accounts.setPassword",
"memberof": "Accounts",
@@ -365,6 +505,38 @@ DocsData = {
"scope": "static",
"summary": "Forcibly change the password for a user."
},
+ "Accounts.setUsername": {
+ "filepath": "accounts-password/password_server.js",
+ "kind": "function",
+ "lineno": 372,
+ "locus": "Server",
+ "longname": "Accounts.setUsername",
+ "memberof": "Accounts",
+ "name": "setUsername",
+ "options": [],
+ "params": [
+ {
+ "description": "The ID of the user to update.
",
+ "name": "userId",
+ "type": {
+ "names": [
+ "String"
+ ]
+ }
+ },
+ {
+ "description": "A new username for the user.
",
+ "name": "newUsername",
+ "type": {
+ "names": [
+ "String"
+ ]
+ }
+ }
+ ],
+ "scope": "static",
+ "summary": "Change a user's username. Use this instead of updating the\ndatabase directly. The operation will fail if there is an existing user\nwith a username only differing in case."
+ },
"Accounts.ui": {
"filepath": "accounts-ui-unstyled/accounts_ui.js",
"kind": "namespace",
@@ -437,7 +609,7 @@ DocsData = {
"Accounts.verifyEmail": {
"filepath": "accounts-password/password_client.js",
"kind": "function",
- "lineno": 259,
+ "lineno": 257,
"locus": "Client",
"longname": "Accounts.verifyEmail",
"memberof": "Accounts",
diff --git a/docs/client/full-api/api/passwords.md b/docs/client/full-api/api/passwords.md
index f109e3cc9d..2f32c71aad 100644
--- a/docs/client/full-api/api/passwords.md
+++ b/docs/client/full-api/api/passwords.md
@@ -31,9 +31,10 @@ id.
On the client, you must pass `password` and at least one of `username` or
`email` — enough information for the user to be able to log in again
-later. If there are existing users with a username or email only differing in case, `createUser` will fail. On the server, you do not need to specify `password`, but the user will
-not be able to log in until it has a password (eg, set with
-[`Accounts.setPassword`](#accounts_setpassword)).
+later. If there are existing users with a username or email only differing in
+case, `createUser` will fail. On the server, you do not need to specify
+`password`, but the user will not be able to log in until it has a password (eg,
+set with [`Accounts.setPassword`](#accounts_setpassword)).
To create an account without a password on the server and still let the
user pick their own password, call `createUser` with the `email` option
@@ -47,6 +48,35 @@ override this behavior, use [`Accounts.onCreateUser`](#accounts_oncreateuser).
This function is only used for creating users with passwords. The external
service login flows do not use this function.
+### Managing usernames and emails
+
+Instead of modifying documents in the [`Meteor.users`](#meteor_users) collection
+directly, use these convenience functions which correctly check for case
+insensitive duplicates before updates.
+
+{{> autoApiBox "Accounts.setUsername"}}
+
+{{> autoApiBox "Accounts.addEmail"}}
+
+By default, an email address is added with `{ verified: false }`. Use
+[`Accounts.sendVerificationEmail`](#Accounts-sendVerificationEmail) to send an
+email with a link the user can use verify their email address.
+
+{{> autoApiBox "Accounts.removeEmail"}}
+
+{{> autoApiBox "Accounts.verifyEmail"}}
+
+This function accepts tokens passed into the callback registered with
+[`Accounts.onEmailVerificationLink`](#Accounts-onEmailVerificationLink).
+
+{{> autoApiBox "Accounts.findUserByUsername"}}
+
+{{> autoApiBox "Accounts.findUserByEmail"}}
+
+### Managing passwords
+
+Use the below functions to initiate password changes or resets from the server
+or the client.
{{> autoApiBox "Accounts.changePassword"}}
@@ -70,10 +100,9 @@ This function accepts tokens passed into the callbacks registered with
{{> autoApiBox "Accounts.setPassword"}}
-{{> autoApiBox "Accounts.verifyEmail"}}
-This function accepts tokens passed into the callback registered with
-[`Accounts.onEmailVerificationLink`](#Accounts-onEmailVerificationLink).
+
+Sending emails
{{> autoApiBox "Accounts.sendResetPasswordEmail"}}
diff --git a/docs/client/full-api/tableOfContents.js b/docs/client/full-api/tableOfContents.js
index 60f4b7a025..12a1bfdda2 100644
--- a/docs/client/full-api/tableOfContents.js
+++ b/docs/client/full-api/tableOfContents.js
@@ -138,11 +138,20 @@ var toc = [
{name: "Passwords", id: "accounts_passwords"}, [
"Accounts.createUser",
+ {type: "spacer"},
+
+ {name: "Accounts.setUsername", id: "Accounts-setUsername"},
+ {name: "Accounts.addEmail", id: "Accounts-addEmail"},
+ {name: "Accounts.removeEmail", id: "Accounts-removeEmail"},
+ {name: "Accounts.verifyEmail", id: "accounts_verifyemail"},
+ {name: "Accounts.findUserByUsername", id: "Accounts-findUserByUsername"},
+ {name: "Accounts.findUserByEmail", id: "Accounts-findUserByEmail"},
+ {type: "spacer"},
+
"Accounts.changePassword",
"Accounts.forgotPassword",
"Accounts.resetPassword",
"Accounts.setPassword",
- "Accounts.verifyEmail",
{type: "spacer"},
"Accounts.sendResetPasswordEmail",
diff --git a/docs/client/names.json b/docs/client/names.json
index 30b9d14c4e..051374eea4 100644
--- a/docs/client/names.json
+++ b/docs/client/names.json
@@ -1,15 +1,20 @@
[
"Accounts",
"Accounts",
+ "Accounts.addEmail",
"Accounts.changePassword",
"Accounts.createUser",
"Accounts.emailTemplates",
+ "Accounts.findUserByEmail",
+ "Accounts.findUserByUsername",
"Accounts.forgotPassword",
+ "Accounts.removeEmail",
"Accounts.resetPassword",
"Accounts.sendEnrollmentEmail",
"Accounts.sendResetPasswordEmail",
"Accounts.sendVerificationEmail",
"Accounts.setPassword",
+ "Accounts.setUsername",
"Accounts.ui",
"Accounts.ui.config",
"Accounts.verifyEmail",
diff --git a/packages/accounts-password/password_client.js b/packages/accounts-password/password_client.js
index ab6cda5e1c..b6aa443695 100644
--- a/packages/accounts-password/password_client.js
+++ b/packages/accounts-password/password_client.js
@@ -130,8 +130,6 @@ Accounts.createUser = function (options, callback) {
});
};
-
-
// Change password. Must be logged in.
//
// @param oldPassword {String|null} By default servers no longer allow
diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js
index cc4a7335de..6e7c1f8f76 100644
--- a/packages/accounts-password/password_server.js
+++ b/packages/accounts-password/password_server.js
@@ -77,13 +77,7 @@ var checkPassword = Accounts._checkPassword;
/// LOGIN
///
-// Attempts to find a user from a user query.
-// First tries to match username or email case sensitively; if that fails, it
-// tries case insensitively; but if more than one user matches the case
-// insensitive search, it returns null
-// @param query {Object} with one of `id`, `username`, or `email`.
-// @returns A user if found, else null
-var findUserFromQuery = function (query) {
+Accounts._findUserByQuery = function (query) {
var user = null;
if (query.id) {
@@ -110,8 +104,6 @@ var findUserFromQuery = function (query) {
// No match if multiple candidates are found
if (candidateUsers.length === 1) {
user = candidateUsers[0];
- } else {
- console.error('Found multiple users with ' + fieldName + ' = ' + fieldValue + ' only differing in case. Requiring case sensitive login.');
}
}
}
@@ -119,6 +111,35 @@ var findUserFromQuery = function (query) {
return user;
};
+/**
+ * @summary Finds the user with the specified username.
+ * First tries to match username case sensitively; if that fails, it
+ * tries case insensitively; but if more than one user matches the case
+ * insensitive search, it returns null.
+ * @locus Server
+ * @param {String} username The username to look for
+ * @returns {Object} A user if found, else null
+ */
+Accounts.findUserByUsername = function (username) {
+ return Accounts._findUserByQuery({
+ username: username
+ });
+};
+
+/**
+ * @summary Finds the user with the specified email.
+ * First tries to match email case sensitively; if that fails, it
+ * tries case insensitively; but if more than one user matches the case
+ * insensitive search, it returns null.
+ * @locus Server
+ * @param {String} email The email address to look for
+ * @returns {Object} A user if found, else null
+ */
+Accounts.findUserByEmail = function (email) {
+ return Accounts._findUserByQuery({
+ email: email
+ });
+};
// Generates a MongoDB selector that can be used to perform a fast case
// insensitive lookup for the given fieldName and string. Since MongoDB does
@@ -164,6 +185,26 @@ var generateCasePermutationsForString = function (string) {
return permutations;
}
+var checkForCaseInsensitiveDuplicates = function (fieldName, displayName, fieldValue, ownUserId) {
+ // Some tests need the ability to add users with the same case insensitive
+ // value, hence the _skipCaseInsensitiveChecksForTest check
+ var skipCheck = _.has(Accounts._skipCaseInsensitiveChecksForTest, fieldValue);
+
+ if (fieldValue && !skipCheck) {
+ var matchedUsers = Meteor.users.find(
+ selectorForFastCaseInsensitiveLookup(fieldName, fieldValue)).fetch();
+
+ if (matchedUsers.length > 0 &&
+ // If we don't have a userId yet, any match we find is a duplicate
+ (!ownUserId ||
+ // Otherwise, check to see if there are multiple matches or a match
+ // that is not us
+ (matchedUsers.length > 1 || matchedUsers[0]._id !== ownUserId))) {
+ throw new Meteor.Error(403, displayName + " already exists.");
+ }
+ }
+};
+
// XXX maybe this belongs in the check package
var NonEmptyString = Match.Where(function (x) {
check(x, String);
@@ -210,7 +251,7 @@ Accounts.registerLoginHandler("password", function (options) {
});
- var user = findUserFromQuery(options.user);
+ var user = Accounts._findUserByQuery(options.user);
if (!user)
throw new Meteor.Error(403, "User not found");
@@ -276,7 +317,7 @@ Accounts.registerLoginHandler("password", function (options) {
password: passwordValidator
});
- var user = findUserFromQuery(options.user);
+ var user = Accounts._findUserByQuery(options.user);
if (!user)
throw new Meteor.Error(403, "User not found");
@@ -320,6 +361,40 @@ Accounts.registerLoginHandler("password", function (options) {
/// CHANGING
///
+/**
+ * @summary Change a user's username. Use this instead of updating the
+ * database directly. The operation will fail if there is an existing user
+ * with a username only differing in case.
+ * @locus Server
+ * @param {String} userId The ID of the user to update.
+ * @param {String} newUsername A new username for the user.
+ */
+Accounts.setUsername = function (userId, newUsername) {
+ check(userId, NonEmptyString);
+ check(newUsername, NonEmptyString);
+
+ var user = Meteor.users.findOne(userId);
+ if (!user)
+ throw new Meteor.Error(403, "User not found");
+
+ var oldUsername = user.username;
+
+ // Perform a case insensitive check fro duplicates before update
+ checkForCaseInsensitiveDuplicates('username', 'Username', newUsername, user._id);
+
+ Meteor.users.update({_id: user._id}, {$set: {username: newUsername}});
+
+ // Perform another check after update, in case a matching user has been
+ // inserted in the meantime
+ try {
+ checkForCaseInsensitiveDuplicates('username', 'Username', newUsername, user._id);
+ } catch (ex) {
+ // Undo update if the check fails
+ Meteor.users.update({_id: user._id}, {$set: {username: oldUsername}});
+ throw ex;
+ }
+};
+
// Let the user change their own password if they know the old
// password. `oldPassword` and `newPassword` should be objects with keys
// `digest` and `algorithm` (representing the SHA256 of the password).
@@ -758,7 +833,111 @@ Meteor.methods({verifyEmail: function (token) {
);
}});
+/**
+ * @summary Add an email address for a user. Use this instead of directly
+ * updating the database. The operation will fail if there is a different user
+ * with an email only differing in case. If the specified user has an existing
+ * email only differing in case however, we replace it.
+ * @locus Server
+ * @param {String} userId The ID of the user to update.
+ * @param {String} newEmail A new email address for the user.
+ * @param {Boolean} [verified] Optional - whether the new email address should
+ * be marked as verified. Defaults to false.
+ */
+Accounts.addEmail = function (userId, newEmail, verified) {
+ check(userId, NonEmptyString);
+ check(newEmail, NonEmptyString);
+ check(verified, Match.Optional(Boolean));
+ if (_.isUndefined(verified)) {
+ verified = false;
+ }
+
+ var user = Meteor.users.findOne(userId);
+ if (!user)
+ throw new Meteor.Error(403, "User not found");
+
+ // Allow users to change their own email to a version with a different case
+
+ // We don't have to call checkForCaseInsensitiveDuplicates to do a case
+ // insensitive check across all emails in the database here because: (1) if
+ // there is no case-insensitive duplicate between this user and other users,
+ // then we are OK and (2) if this would create a conflict with other users
+ // then there would already be a case-insensitive duplicate and we can't fix
+ // that in this code anyway.
+ var caseInsensitiveRegExp =
+ new RegExp('^' + Meteor._escapeRegExp(newEmail) + '$', 'i');
+
+ var didUpdateOwnEmail = _.any(user.emails, function(email, index) {
+ if (caseInsensitiveRegExp.test(email.address)) {
+ Meteor.users.update({
+ _id: user._id,
+ 'emails.address': email.address
+ }, {$set: {
+ 'emails.$.address': newEmail,
+ 'emails.$.verified': verified
+ }});
+ return true;
+ }
+
+ return false;
+ });
+
+ // In the other updates below, we have to do another call to
+ // checkForCaseInsensitiveDuplicates to make sure that no conflicting values
+ // were added to the database in the meantime. We don't have to do this for
+ // the case where the user is updating their email address to one that is the
+ // same as before, but only different because of capitalization. Read the
+ // big comment above to understand why.
+
+ if (didUpdateOwnEmail) {
+ return;
+ }
+
+ // Perform a case insensitive check for duplicates before update
+ checkForCaseInsensitiveDuplicates('emails.address', 'Email', newEmail, user._id);
+
+ Meteor.users.update({
+ _id: user._id
+ }, {
+ $addToSet: {
+ emails: {
+ address: newEmail,
+ verified: verified
+ }
+ }
+ });
+
+ // Perform another check after update, in case a matching user has been
+ // inserted in the meantime
+ try {
+ checkForCaseInsensitiveDuplicates('emails.address', 'Email', newEmail, user._id);
+ } catch (ex) {
+ // Undo update if the check fails
+ Meteor.users.update({_id: user._id},
+ {$pull: {emails: {address: newEmail}}});
+ throw ex;
+ }
+}
+
+/**
+ * @summary Remove an email address for a user. Use this instead of updating
+ * the database directly.
+ * @locus Server
+ * @param {String} userId The ID of the user to update.
+ * @param {String} email The email address to remove.
+ */
+Accounts.removeEmail = function (userId, email) {
+ check(userId, NonEmptyString);
+ check(email, NonEmptyString);
+
+ var user = Meteor.users.findOne(userId);
+ if (!user)
+ throw new Meteor.Error(403, "User not found");
+
+ Meteor.users.update({_id: user._id},
+ {$pull: {emails: {address: email}}});
+}
///
/// CREATING USERS
@@ -794,34 +973,16 @@ var createUser = function (options) {
if (email)
user.emails = [{address: email, verified: false}];
- // Check if there is no other user with a username or email only differing
- // in case.
- var performCaseInsensitiveCheck = function () {
- // Some tests need the ability to add users with the same case insensitive
- // username or email, hence the _skipCaseInsensitiveChecksForTest check
-
- if (username &&
- !_.has(Accounts._skipCaseInsensitiveChecksForTest, username) &&
- Meteor.users.find(selectorForFastCaseInsensitiveLookup(
- "username", username)).count() > 1) {
- throw new Meteor.Error(403, "Username already exists.");
- }
-
- if (email &&
- !_.has(Accounts._skipCaseInsensitiveChecksForTest, email) &&
- Meteor.users.find(selectorForFastCaseInsensitiveLookup(
- "emails.address", email)).count() > 1) {
- throw new Meteor.Error(403, "Email already exists.");
- }
- }
-
// Perform a case insensitive check before insert
- performCaseInsensitiveCheck();
+ checkForCaseInsensitiveDuplicates('username', 'Username', username);
+ checkForCaseInsensitiveDuplicates('emails.address', 'Email', email);
+
var userId = Accounts.insertUserDoc(options, user);
// Perform another check after insert, in case a matching user has been
// inserted in the meantime
try {
- performCaseInsensitiveCheck();
+ checkForCaseInsensitiveDuplicates('username', 'Username', username, userId);
+ checkForCaseInsensitiveDuplicates('emails.address', 'Email', email, userId);
} catch (ex) {
// Remove inserted user if the check fails
Meteor.users.remove(userId);
diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js
index 32c361d9bb..943062b74d 100644
--- a/packages/accounts-password/password_tests.js
+++ b/packages/accounts-password/password_tests.js
@@ -2,9 +2,6 @@ Accounts._noConnectionCloseDelayForTest = true;
Accounts.removeDefaultRateLimit();
if (Meteor.isServer) {
Meteor.methods({
- getUserId: function () {
- return this.userId;
- },
getResetToken: function () {
var token = Meteor.users.findOne(this.userId).services.password.reset;
return token;
@@ -48,14 +45,29 @@ if (Meteor.isClient) (function () {
};
var logoutStep = function (test, expect) {
Meteor.logout(expect(function (error) {
- test.equal(error, undefined);
+ if (error) {
+ test.fail(error.message);
+ }
test.equal(Meteor.user(), null);
}));
};
var loggedInAs = function (someUsername, test, expect) {
return expect(function (error) {
- test.equal(error, undefined);
- test.equal(Meteor.user().username, someUsername);
+ if (error) {
+ test.fail(error.message);
+ }
+ test.equal(Meteor.userId() && Meteor.user().username, someUsername);
+ });
+ };
+ var loggedInUserHasEmail = function (someEmail, test, expect) {
+ return expect(function (error) {
+ if (error) {
+ test.fail(error.message);
+ }
+ var user = Meteor.user();
+ test.isTrue(user && _.some(user.emails, function(email) {
+ return email.address === someEmail;
+ }));
});
};
var expectError = function (expectedError, test, expect) {
@@ -74,17 +86,23 @@ if (Meteor.isClient) (function () {
};
var invalidateLoginsStep = function (test, expect) {
Meteor.call("testInvalidateLogins", 'fail', expect(function (error) {
- test.isFalse(error);
+ if (error) {
+ test.fail(error.message);
+ }
}));
};
var hideActualLoginErrorStep = function (test, expect) {
Meteor.call("testInvalidateLogins", 'hide', expect(function (error) {
- test.isFalse(error);
+ if (error) {
+ test.fail(error.message);
+ }
}));
};
var validateLoginsStep = function (test, expect) {
Meteor.call("testInvalidateLogins", false, expect(function (error) {
- test.isFalse(error);
+ if (error) {
+ test.fail(error.message);
+ }
}));
};
@@ -206,7 +224,8 @@ if (Meteor.isClient) (function () {
}
]);
- testAsyncMulti("passwords - logging in with case insensitive username with non-ASCII characters", [
+ testAsyncMulti("passwords - logging in with case insensitive username " +
+ "with non-ASCII characters", [
function (test, expect) {
// Hack because Tinytest does not clean the database between tests/runs
this.randomSuffix = Random.id(10);
@@ -226,7 +245,8 @@ if (Meteor.isClient) (function () {
}
]);
- testAsyncMulti("passwords - logging in with case insensitive username should escape regex special characters", [
+ testAsyncMulti("passwords - logging in with case insensitive username " +
+ "should escape regex special characters", [
createUserStep,
logoutStep,
// We shouldn't be able to log in with a regex expression for the username
@@ -238,7 +258,8 @@ if (Meteor.isClient) (function () {
}
]);
- testAsyncMulti("passwords - logging in with case insensitive username should require a match of the full string", [
+ testAsyncMulti("passwords - logging in with case insensitive username " +
+ "should require a match of the full string", [
createUserStep,
logoutStep,
// We shouldn't be able to log in with a partial match for the username
@@ -250,21 +271,22 @@ if (Meteor.isClient) (function () {
}
]);
- testAsyncMulti("passwords - logging in with case insensitive username when there are multiple matches", [
+ testAsyncMulti("passwords - logging in with case insensitive username when " +
+ "there are multiple matches", [
createUserStep,
logoutStep,
function (test, expect) {
- this.otherUserName = 'Adalovelace' + this.randomSuffix;
- addSkipCaseInsensitiveChecksForTest(this.otherUserName, test, expect);
+ this.otherUsername = 'Adalovelace' + this.randomSuffix;
+ addSkipCaseInsensitiveChecksForTest(this.otherUsername, test, expect);
},
// Create another user with a username that only differs in case
function (test, expect) {
Accounts.createUser(
- { username: this.otherUserName, password: this.password },
- loggedInAs(this.otherUserName, test, expect));
+ { username: this.otherUsername, password: this.password },
+ loggedInAs(this.otherUsername, test, expect));
},
function (test, expect) {
- removeSkipCaseInsensitiveChecksForTest(this.otherUserName, test, expect);
+ removeSkipCaseInsensitiveChecksForTest(this.otherUsername, test, expect);
},
// We shouldn't be able to log in with the username in lower case
function (test, expect) {
@@ -282,7 +304,8 @@ if (Meteor.isClient) (function () {
}
]);
- testAsyncMulti("passwords - creating users with the same case insensitive username", [
+ testAsyncMulti("passwords - creating users with the same case insensitive " +
+ "username", [
createUserStep,
logoutStep,
// Attempting to create another user with a username that only differs in
@@ -318,7 +341,8 @@ if (Meteor.isClient) (function () {
}
]);
- testAsyncMulti("passwords - logging in with case insensitive email should escape regex special characters", [
+ testAsyncMulti("passwords - logging in with case insensitive email should " +
+ "escape regex special characters", [
createUserStep,
logoutStep,
// We shouldn't be able to log in with a regex expression for the email
@@ -330,7 +354,8 @@ if (Meteor.isClient) (function () {
}
]);
- testAsyncMulti("passwords - logging in with case insensitive email should require a match of the full string", [
+ testAsyncMulti("passwords - logging in with case insensitive email should " +
+ "require a match of the full string", [
createUserStep,
logoutStep,
// We shouldn't be able to log in with a partial match for the email
@@ -342,24 +367,25 @@ if (Meteor.isClient) (function () {
}
]);
- testAsyncMulti("passwords - logging in with case insensitive email when there are multiple matches", [
+ testAsyncMulti("passwords - logging in with case insensitive email when " +
+ "there are multiple matches", [
createUserStep,
logoutStep,
function (test, expect) {
- this.otherUserName = 'AdaLovelace' + Random.id(10);
+ this.otherUsername = 'AdaLovelace' + Random.id(10);
this.otherEmail = "ADA-intercept@lovelace.com" + this.randomSuffix;
addSkipCaseInsensitiveChecksForTest(this.otherEmail, test, expect);
},
// Create another user with an email that only differs in case
function (test, expect) {
Accounts.createUser(
- { username: this.otherUserName,
+ { username: this.otherUsername,
email: this.otherEmail,
password: this.password },
- loggedInAs(this.otherUserName, test, expect));
+ loggedInAs(this.otherUsername, test, expect));
},
function (test, expect) {
- removeSkipCaseInsensitiveChecksForTest(this.otherUserName, test, expect);
+ removeSkipCaseInsensitiveChecksForTest(this.otherUsername, test, expect);
},
logoutStep,
// We shouldn't be able to log in with the email in lower case
@@ -378,7 +404,8 @@ if (Meteor.isClient) (function () {
}
]);
- testAsyncMulti("passwords - creating users with the same case insensitive email", [
+ testAsyncMulti("passwords - creating users with the same case insensitive " +
+ "email", [
createUserStep,
logoutStep,
// Attempting to create another user with an email that only differs in
@@ -1229,7 +1256,8 @@ if (Meteor.isServer) (function () {
});
});
- // XXX would be nice to test Accounts.config({forbidClientAccountCreation: true})
+ // XXX would be nice to test
+ // Accounts.config({forbidClientAccountCreation: true})
Tinytest.addAsync(
'passwords - login token observes get cleaned up',
@@ -1295,7 +1323,8 @@ if (Meteor.isServer) (function () {
Accounts.sendResetPasswordEmail(userId, email);
- var resetPasswordEmailOptions = Meteor.call("getInterceptedEmails", email)[0];
+ var resetPasswordEmailOptions =
+ Meteor.call("getInterceptedEmails", email)[0];
var re = new RegExp(Meteor.absoluteUrl() + "#/reset-password/(\\S*)");
var match = resetPasswordEmailOptions.text.match(re);
@@ -1312,4 +1341,164 @@ if (Meteor.isServer) (function () {
Meteor.call("login", {user: {username: username}, password: "new-password"});
}, /Incorrect password/);
});
+
+ // We should be able to change the username
+ Tinytest.add("passwords - change username", function (test) {
+ var username = Random.id();
+ var userId = Accounts.createUser({
+ username: username
+ });
+
+ test.isTrue(userId);
+
+ var newUsername = Random.id();
+ Accounts.setUsername(userId, newUsername);
+
+ test.equal(Accounts._findUserByQuery({id: userId}).username, newUsername);
+
+ // Test findUserByUsername as well while we're here
+ test.equal(Accounts.findUserByUsername(newUsername)._id, userId);
+ });
+
+ Tinytest.add("passwords - change username to a new one only differing " +
+ "in case", function (test) {
+ var username = Random.id() + "user";
+ var userId = Accounts.createUser({
+ username: username.toUpperCase()
+ });
+
+ test.isTrue(userId);
+
+ var newUsername = username.toLowerCase();
+ Accounts.setUsername(userId, newUsername);
+
+ test.equal(Accounts._findUserByQuery({id: userId}).username, newUsername);
+ });
+
+ // We should not be able to change the username to one that only
+ // differs in case from an existing one
+ Tinytest.add("passwords - change username should fail when there are " +
+ "existing users with a username only differing in case", function (test) {
+ var username = Random.id() + "user";
+ var usernameUpper = username.toUpperCase();
+
+ var userId1 = Accounts.createUser({
+ username: username
+ });
+
+ var user2OriginalUsername = Random.id();
+ var userId2 = Accounts.createUser({
+ username: user2OriginalUsername
+ });
+
+ test.isTrue(userId1);
+ test.isTrue(userId2);
+
+ test.throws(function () {
+ Accounts.setUsername(userId2, usernameUpper);
+ }, /Username already exists/);
+
+ test.equal(Accounts._findUserByQuery({id: userId2}).username,
+ user2OriginalUsername);
+ });
+
+ Tinytest.add("passwords - add email", function (test) {
+ var origEmail = Random.id() + "@turing.com";
+ var userId = Accounts.createUser({
+ email: origEmail
+ });
+
+ var newEmail = Random.id() + "@turing.com";
+ Accounts.addEmail(userId, newEmail);
+
+ var thirdEmail = Random.id() + "@turing.com";
+ Accounts.addEmail(userId, thirdEmail, true);
+
+ test.equal(Accounts._findUserByQuery({id: userId}).emails, [
+ { address: origEmail, verified: false },
+ { address: newEmail, verified: false },
+ { address: thirdEmail, verified: true }
+ ]);
+
+ // Test findUserByEmail as well while we're here
+ test.equal(Accounts.findUserByEmail(origEmail)._id, userId);
+ });
+
+ Tinytest.add("passwords - add email when the user has an existing email " +
+ "only differing in case", function (test) {
+ var origEmail = Random.id() + "@turing.com";
+ var userId = Accounts.createUser({
+ email: origEmail
+ });
+
+ var newEmail = Random.id() + "@turing.com";
+ Accounts.addEmail(userId, newEmail);
+
+ var thirdEmail = origEmail.toUpperCase();
+ Accounts.addEmail(userId, thirdEmail, true);
+
+ test.equal(Accounts._findUserByQuery({id: userId}).emails, [
+ { address: thirdEmail, verified: true },
+ { address: newEmail, verified: false }
+ ]);
+ });
+
+ Tinytest.add("passwords - add email should fail when there is an existing " +
+ "user with an email only differing in case", function (test) {
+ var user1Email = Random.id() + "@turing.com";
+ var userId1 = Accounts.createUser({
+ email: user1Email
+ });
+
+ var user2Email = Random.id() + "@turing.com";
+ var userId2 = Accounts.createUser({
+ email: user2Email
+ });
+
+ var dupEmail = user1Email.toUpperCase();
+ test.throws(function () {
+ Accounts.addEmail(userId2, dupEmail);
+ }, /Email already exists/);
+
+ test.equal(Accounts._findUserByQuery({id: userId1}).emails, [
+ { address: user1Email, verified: false }
+ ]);
+
+ test.equal(Accounts._findUserByQuery({id: userId2}).emails, [
+ { address: user2Email, verified: false }
+ ]);
+ });
+
+ Tinytest.add("passwords - remove email", function (test) {
+ var origEmail = Random.id() + "@turing.com";
+ var userId = Accounts.createUser({
+ email: origEmail
+ });
+
+ var newEmail = Random.id() + "@turing.com";
+ Accounts.addEmail(userId, newEmail);
+
+ var thirdEmail = Random.id() + "@turing.com";
+ Accounts.addEmail(userId, thirdEmail, true);
+
+ test.equal(Accounts._findUserByQuery({id: userId}).emails, [
+ { address: origEmail, verified: false },
+ { address: newEmail, verified: false },
+ { address: thirdEmail, verified: true }
+ ]);
+
+ Accounts.removeEmail(userId, newEmail);
+
+ test.equal(Accounts._findUserByQuery({id: userId}).emails, [
+ { address: origEmail, verified: false },
+ { address: thirdEmail, verified: true }
+ ]);
+
+ Accounts.removeEmail(userId, origEmail);
+
+ test.equal(Accounts._findUserByQuery({id: userId}).emails, [
+ { address: thirdEmail, verified: true }
+ ]);
+ });
+
}) ();