Add case-insensitive accounts-password utility functions

Since we have added additional constraints to the database around case
sensitivity, we now want to discourage people from working with the Accounts
collection directly and provide an API for changing certain fields in a correct
way.

Methods that do database checks before and after the operation:
  Accounts.setUsername
  Accounts.addEmail
  Accounts.removeEmail

Methods that make sure to use a case-insensitive query to retrieve the user
  Accounts.findUserByUsername
  Accounts.findUserByEmail

PR #5024
This commit is contained in:
Martijn Walraven
2015-08-11 08:50:50 +02:00
committed by Sashko Stubailo
parent c237c37618
commit dac019fda8
7 changed files with 643 additions and 80 deletions

View File

@@ -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": "<p>The ID of the user to update.</p>",
"name": "userId",
"type": {
"names": [
"String"
]
}
},
{
"description": "<p>A new email address for the user.</p>",
"name": "newEmail",
"type": {
"names": [
"String"
]
}
},
{
"description": "<p>Optional - whether the new email address should\nbe marked as verified. Defaults to false.</p>",
"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": "<p>The email address to look for</p>",
"name": "email",
"type": {
"names": [
"String"
]
}
}
],
"returns": [
{
"description": "<p>A user if found, else null</p>",
"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": "<p>The username to look for</p>",
"name": "username",
"type": {
"names": [
"String"
]
}
}
],
"returns": [
{
"description": "<p>A user if found, else null</p>",
"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": "<p>The ID of the user to update.</p>",
"name": "userId",
"type": {
"names": [
"String"
]
}
},
{
"description": "<p>The email address to remove.</p>",
"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": "<p>The ID of the user to update.</p>",
"name": "userId",
"type": {
"names": [
"String"
]
}
},
{
"description": "<p>A new username for the user.</p>",
"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",

View File

@@ -31,9 +31,10 @@ id.
On the client, you must pass `password` and at least one of `username` or
`email` &mdash; 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).
<h3 id="sending-emails"><span>Sending emails</span></h3>
{{> autoApiBox "Accounts.sendResetPasswordEmail"}}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

View File

@@ -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);

View File

@@ -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 }
]);
});
}) ();