Files
meteor/packages/accounts-password/password_tests.js

2058 lines
68 KiB
JavaScript

Accounts._connectionCloseDelayMsForTests = 1000;
const makeTestConnAsync =
(test) =>
new Promise((resolve, reject) => {
makeTestConnection(
test,
(clientConn, serverConn) => resolve({ clientConn, serverConn }),
reject
);
})
const simplePollAsync = (fn) =>
new Promise((resolve, reject) => simplePoll(fn,resolve,reject))
function hashPasswordWithSha(password) {
return {
digest: SHA256(password),
algorithm: "sha-256"
};
}
if (Meteor.isServer) {
Accounts.removeDefaultRateLimit();
Meteor.methods(
{
getResetToken:
async function () {
const user = await Meteor.users.findOneAsync(this.userId);
return user.services.password.reset;
},
addSkipCaseInsensitiveChecksForTest:
value => Accounts._skipCaseInsensitiveChecksForTest[value] = true,
removeSkipCaseInsensitiveChecksForTest:
value => delete Accounts._skipCaseInsensitiveChecksForTest[value],
async countUsersOnServer(query) {
return await Meteor.users.find(query).countAsync();
}
}
);
}
if (Meteor.isClient) (() => {
// XXX note, only one test can do login/logout things at once! for
// now, that is this test.
Accounts._isolateLoginTokenForTest();
const addSkipCaseInsensitiveChecksForTest = (value, test, expect) =>
Meteor.call('addSkipCaseInsensitiveChecksForTest', value);
const removeSkipCaseInsensitiveChecksForTest = (value, test, expect) =>
Meteor.call('removeSkipCaseInsensitiveChecksForTest', value);
// Make logout steps awaitable so subsequent test steps don't race.
const logoutStep = async (test, expect) =>
new Promise(resolve => {
Meteor.logout(err => {
if (err) {
// keep original behavior: fail the test if logout errored
test.fail(err.message);
// still resolve so test runner can continue
return resolve();
}
test.equal(Meteor.user(), null);
resolve();
});
});
// Create user only after a confirmed logout to avoid races between
// tests that do login/logout operations.
const createUserStep = async function (test, expect) {
// Wait for the logout to complete synchronously.
await logoutStep(test, expect);
// Hack because Tinytest does not clean the database between tests/runs
this.randomSuffix = Random.id(10);
this.username = `AdaLovelace${ this.randomSuffix }`;
this.email = `Ada-intercept@lovelace.com${ this.randomSuffix }`;
this.password = 'password';
Accounts.createUser(
{ username: this.username, email: this.email, password: this.password },
loggedInAs(this.username, test, expect));
};
const loggedInAs = (someUsername, test, expect) => {
return expect(error => {
if (error) {
test.fail(error.message);
}
test.equal(Meteor.userId() && Meteor.user().username, someUsername);
});
};
const expectError = (expectedError, test, expect) => expect(actualError => {
test.equal(actualError && actualError.error, expectedError.error);
test.equal(actualError && actualError.reason, expectedError.reason);
});
const expectUserNotFound = (test, expect) =>
expectError(new Meteor.Error(403, "User not found"), test, expect);
const waitForLoggedOutStep = (test, expect) => pollUntil(
expect,
() => Meteor.userId() === null,
10 * 1000,
100
);
const invalidateLoginsStep = (test, expect) =>
Meteor.call("testInvalidateLogins", 'fail', expect(error => {
if (error) {
test.fail(error.message);
}
}));
const hideActualLoginErrorStep = (test, expect) =>
Meteor.call("testInvalidateLogins", 'hide', expect(error => {
if (error) {
test.fail(error.message);
}
}));
const validateLoginsStep = (test, expect) =>
Meteor.call("testInvalidateLogins", false, expect(error => {
if (error) {
test.fail(error.message);
}
}));
testAsyncMulti("passwords - basic login with password", [
function (test, expect) {
// setup
this.username = Random.id();
this.email = `${ Random.id() }-intercept@example.com`;
this.password = 'password';
Accounts.createUser(
{ username: this.username, email: this.email, password: this.password },
loggedInAs(this.username, test, expect));
},
function (test, expect) {
test.notEqual(Meteor.userId(), null);
},
logoutStep,
function (test, expect) {
Meteor.loginWithPassword(this.username, this.password,
loggedInAs(this.username, test, expect));
},
logoutStep,
// This next step tests reactive contexts which are reactive on
// Meteor.user().
function (test, expect) {
// Set up a reactive context that only refreshes when Meteor.user() is
// invalidated.
let loaded = false;
const handle = Tracker.autorun(() => {
if (Meteor.user() && Meteor.user().emails)
loaded = true;
});
// At the beginning, we're not logged in.
test.isFalse(loaded);
Meteor.loginWithPassword(this.username, this.password, expect(error => {
test.equal(error, undefined);
test.notEqual(Meteor.userId(), null);
// By the time of the login callback, the user should be loaded.
test.isTrue(Meteor.user().emails);
// Flushing should get us the rerun as well.
Tracker.flush();
test.isTrue(loaded);
handle.stop();
}));
},
logoutStep,
function (test, expect) {
Meteor.loginWithPassword({ username: this.username }, this.password,
loggedInAs(this.username, test, expect));
},
logoutStep,
function (test, expect) {
Meteor.loginWithPassword(this.email, this.password,
loggedInAs(this.username, test, expect));
},
logoutStep,
function (test, expect) {
Meteor.loginWithPassword({ email: this.email }, this.password,
loggedInAs(this.username, test, expect));
},
logoutStep
]);
testAsyncMulti("passwords - logging in with case insensitive username", [
createUserStep,
logoutStep,
// We should be able to log in with the username in lower case
function (test, expect) {
Meteor.loginWithPassword(
{ username: `adalovelace${ this.randomSuffix }` },
this.password,
loggedInAs(this.username, test, expect));
}
]);
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);
this.username = `ÁdaLØvela😈e${ this.randomSuffix }`;
this.password = 'password';
Accounts.createUser(
{ username: this.username, email: this.email, password: this.password },
loggedInAs(this.username, test, expect));
},
logoutStep,
// We should be able to log in with the username in lower case
function (test, expect) {
Meteor.loginWithPassword(
{ username: `ádaløvela😈e${ this.randomSuffix }` },
this.password,
loggedInAs(this.username, test, expect));
}
]);
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
function (test, expect) {
Meteor.loginWithPassword(
{ username: `.+${ this.randomSuffix }` },
this.password,
expectUserNotFound(test, expect));
}
]);
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
function (test, expect) {
Meteor.loginWithPassword(
{ username: `lovelace${ this.randomSuffix }` },
this.password,
expectUserNotFound(test, expect));
}
]);
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);
},
// 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));
},
function (test, expect) {
removeSkipCaseInsensitiveChecksForTest(this.otherUsername, test, expect);
},
// We shouldn't be able to log in with the username in lower case
function (test, expect) {
Meteor.loginWithPassword(
{ username: `adalovelace${ this.randomSuffix }` },
this.password,
expectUserNotFound(test, expect));
},
// We should still be able to log in with the username in original case
function (test, expect) {
Meteor.loginWithPassword(
{ username: this.username },
this.password,
loggedInAs(this.username, test, expect));
}
]);
testAsyncMulti("passwords - creating users with the same case insensitive " +
"username", [
createUserStep,
logoutStep,
// Attempting to create another user with a username that only differs in
// case should fail
function (test, expect) {
this.newUsername = `adalovelace${ this.randomSuffix }`;
Accounts.createUser(
{ username: this.newUsername, password: this.password },
expectError(
new Meteor.Error(403, "Username already exists."),
test,
expect));
},
// Make sure the new user has not been inserted
async function (test) {
const result = await Meteor.callAsync('countUsersOnServer', {
username: this.newUsername,
});
test.equal(result, 0);
}
]);
testAsyncMulti("passwords - logging in with case insensitive email", [
createUserStep,
logoutStep,
// We should be able to log in with the email in lower case
function (test, expect) {
Meteor.loginWithPassword(
{ email: `ada-intercept@lovelace.com${ this.randomSuffix }` },
this.password,
loggedInAs(this.username, test, expect));
}
]);
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
function (test, expect) {
Meteor.loginWithPassword(
{ email: `.+${ this.randomSuffix }` },
this.password,
expectUserNotFound(test, expect));
}
]);
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
function (test, expect) {
Meteor.loginWithPassword(
{ email: `com${ this.randomSuffix }` },
this.password,
expectUserNotFound(test, expect));
}
]);
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.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,
email: this.otherEmail,
password: this.password
},
loggedInAs(this.otherUsername, test, expect));
},
function (test, expect) {
removeSkipCaseInsensitiveChecksForTest(this.otherUsername, test, expect);
},
logoutStep,
// We shouldn't be able to log in with the email in lower case
function (test, expect) {
Meteor.loginWithPassword(
{ email: `ada-intercept@lovelace.com${ this.randomSuffix }` },
this.password,
expectUserNotFound(test, expect));
},
// We should still be able to log in with the email in original case
function (test, expect) {
Meteor.loginWithPassword(
{ email: this.email },
this.password,
loggedInAs(this.username, test, expect));
}
]);
testAsyncMulti("passwords - creating users with the same case insensitive " +
"email", [
createUserStep,
logoutStep,
// Create user error without callback should throw error
function (test, expect) {
this.newUsername = `adalovelace${ this.randomSuffix }`;
test.throws(function () {
Accounts.createUser({ username: this.newUsername, password: '' });
}, /Password may not be empty/);
},
// Attempting to create another user with an email that only differs in
// case should fail
function (test, expect) {
this.newEmail = `ada-intercept@lovelace.com${ this.randomSuffix }`;
Accounts.createUser(
{ email: this.newEmail, password: this.password },
expectError(
new Meteor.Error(403, "Email already exists."),
test,
expect));
},
// Make sure the new user has not been inserted
async function (test) {
const result = await Meteor.callAsync('countUsersOnServer', {
'emails.address': this.newEmail,
});
test.equal(result, 0);
}
]);
testAsyncMulti("passwords - changing passwords", [
function (test, expect) {
// setup
this.username = Random.id();
this.email = `${ Random.id() }-intercept@example.com`;
this.password = 'password';
this.password2 = 'password2';
Accounts.createUser(
{ username: this.username, email: this.email, password: this.password },
loggedInAs(this.username, test, expect));
},
// Send a password reset email so that we can test that password
// reset tokens get deleted on password change.
function (test, expect) {
Meteor.call("forgotPassword",
{ email: this.email }, expect(error => {
test.isFalse(error);
}));
},
async function (test) {
const token = await Meteor.callAsync("getResetToken");
test.isTrue(token);
this.token = token;
},
// change password with bad old password. we stay logged in.
function (test, expect) {
Accounts.changePassword('wrong', 'doesntmatter', expect(error => {
test.isTrue(error);
test.equal(Meteor.user().username, this.username);
}));
},
// change password with blank new password
function (test, expect) {
test.throws(
() => Accounts.changePassword(this.password, ''),
/Password may not be empty/
);
},
// change password with good old password.
function (test, expect) {
Accounts.changePassword(this.password, this.password2,
loggedInAs(this.username, test, expect));
},
async function (test) {
const token = await Meteor.callAsync("getResetToken");
test.isFalse(token);
},
logoutStep,
// old password, failed login
function (test, expect) {
Meteor.loginWithPassword(this.email, this.password, expect(error => {
test.isTrue(error);
test.isFalse(Meteor.user());
}));
},
// new password, success
function (test, expect) {
Meteor.loginWithPassword(this.email, this.password2,
loggedInAs(this.username, test, expect));
},
logoutStep
]);
testAsyncMulti("passwords - changing password logs out other clients", [
function (test, expect) {
this.username = Random.id();
this.email = `${ Random.id() }-intercept@example.com`;
this.password = 'password';
this.password2 = 'password2';
Accounts.createUser(
{ username: this.username, email: this.email, password: this.password },
loggedInAs(this.username, test, expect)
);
},
// Log in a second connection as this user.
function (test, expect) {
this.secondConn = DDP.connect(Meteor.absoluteUrl());
this.secondConn.call('login',
{ user: { username: this.username }, password: hashPasswordWithSha(this.password) },
expect((err, result) => {
test.isFalse(err);
this.secondConn.setUserId(result.id);
test.isTrue(this.secondConn.userId());
this.secondConn.onReconnect = () =>
this.secondConn.apply(
'login',
[{ resume: result.token }],
{ wait: true },
(err, result) =>
this.secondConn.setUserId(result && result.id || null)
);
}));
},
function (test, expect) {
Accounts.changePassword(
this.password,
this.password2,
expect(err => test.isFalse(err))
);
},
// Now that we've changed the password, wait until the second
// connection gets logged out.
function (test, expect) {
pollUntil(
expect,
() => this.secondConn.userId() === null,
10 * 1000,
100
);
}
]);
testAsyncMulti("passwords - forgotPassword client return error when empty email", [
function (test, expect) {
// setup
this.email = '';
},
// forgotPassword called on client with blank email
function (test, expect) {
Accounts.forgotPassword(
{ email: this.email },
expect(error => test.isTrue(error))
);
},
// forgotPassword called on client with blank email and no callback.
function (test, expect) {
test.throws(
() => Accounts.forgotPassword({ email: this.email }),
/Must pass options\.email/
);
},
]);
Tinytest.add(
'passwords - forgotPassword only passes callback value to forgotPassword '
+ 'Method if callback is defined (to address issue #5676)',
test => {
let methodCallArgumentCount = 0;
const originalMethodCall = Accounts.connection.call;
const stubMethodCall = (...args) => {
methodCallArgumentCount = args.length;
}
Accounts.connection.call = stubMethodCall;
Accounts.forgotPassword({ email: 'test@meteor.com' });
test.equal(
methodCallArgumentCount,
2,
'Method call should have 2 arguments since no callback is passed in'
);
Accounts.forgotPassword({ email: 'test@meteor.com' }, () => {
});
test.equal(
methodCallArgumentCount,
3,
'Method call should have 3 arguments since a callback is passed in'
);
Accounts.connection.call = originalMethodCall;
}
);
testAsyncMulti("passwords - verifyEmail client return error when empty token", [
function (test, expect) {
// setup
this.token = '';
},
// verifyEmail called on client with blank token
function (test, expect) {
Accounts.verifyEmail(
this.token,
expect(error => test.isTrue(error))
);
},
// verifyEmail called on client with blank token and no callback.
function (test, expect) {
test.throws(
() => Accounts.verifyEmail(this.token),
/Need to pass token/
);
},
]);
testAsyncMulti("passwords - resetPassword errors", [
function (test, expect) {
// setup
this.token = '';
this.newPassword = 'nonblankpassword';
},
// resetPassword called on client with blank token
function (test, expect) {
Accounts.resetPassword(
this.token,
this.newPassword,
expect(error => test.isTrue(error))
);
},
function (test, expect) {
// setup
this.token = 'nonblank-token';
this.newPassword = '';
},
// resetPassword called on client with blank password
function (test, expect) {
Accounts.resetPassword(
this.token,
this.newPassword,
expect(error => test.isTrue(error))
);
},
// resetPassword called on client with blank password and no callback.
function (test, expect) {
test.throws(
() => Accounts.resetPassword(this.token, this.newPassword),
/Password may not be empty/
);
},
]);
testAsyncMulti("passwords - new user hooks", [
function (test, expect) {
// setup
this.username = Random.id();
this.email = `${ Random.id() }-intercept@example.com`;
this.password = 'password';
},
// test Accounts.validateNewUser
function (test, expect) {
Accounts.createUser(
{
username: this.username, password: this.password,
// should fail the new user validators
profile: { invalid: true }
},
expect(error => {
test.equal(error.error, 403);
test.equal(error.reason, "User validation failed");
}));
},
logoutStep,
function (test, expect) {
Accounts.createUser(
{
username: this.username, password: this.password,
// should fail the new user validator with a special
// exception
profile: { invalidAndThrowException: true }
},
expect(error =>
test.equal(
error.reason,
"An exception thrown within Accounts.validateNewUser"
)
)
);
},
// test Accounts.onCreateUser
function (test, expect) {
Accounts.createUser(
{
username: this.username, password: this.password,
testOnCreateUserHook: true
},
loggedInAs(this.username, test, expect));
},
function (test, expect) {
test.equal(Meteor.user().profile.touchedByOnCreateUser, true);
},
logoutStep
]);
testAsyncMulti("passwords - Meteor.user()", [
function (test, expect) {
// setup
this.username = Random.id();
this.password = 'password';
Accounts.createUser(
{
username: this.username, password: this.password,
testOnCreateUserHook: true
},
loggedInAs(this.username, test, expect));
},
// test Meteor.user(). This test properly belongs in
// accounts-base/accounts_tests.js, but this is where the tests that
// actually log in are.
async function (test, expect) {
const clientUser = await Meteor.userAsync();
Accounts.connection.call('testMeteorUser', expect((err, result) => {
test.equal(result._id, clientUser._id);
test.equal(result.username, clientUser.username);
test.equal(result.username, this.username);
test.equal(result.profile.touchedByOnCreateUser, true);
test.equal(err, undefined);
}));
},
async function (test, expect) {
// Test that even with no published fields, we still have a document.
await Accounts.connection.callAsync('clearUsernameAndProfile');
test.isTrue(Meteor.userId());
test.equal(await Meteor.userAsync(), { _id: Meteor.userId() });
},
logoutStep,
function (test, expect) {
const clientUser = Meteor.user();
test.equal(clientUser, null);
test.equal(Meteor.userId(), null);
Accounts.connection.call('testMeteorUser', expect((err, result) => {
test.equal(err, undefined);
test.equal(result, null);
}));
}
]);
testAsyncMulti("passwords - allow rules", [
// create a second user to have an id for in a later test
function (test, expect) {
this.otherUsername = Random.id();
Accounts.createUser(
{
username: this.otherUsername, password: 'dontcare',
testOnCreateUserHook: true
},
loggedInAs(this.otherUsername, test, expect));
},
function (test, expect) {
this.otherUserId = Meteor.userId();
},
function (test, expect) {
// real setup
this.username = Random.id();
this.password = 'password';
Accounts.createUser(
{
username: this.username, password: this.password,
testOnCreateUserHook: true
},
loggedInAs(this.username, test, expect));
},
// test the default Meteor.users allow rule. This test properly belongs in
// accounts-base/accounts_tests.js, but this is where the tests that
// actually log in are.
async function (test, expect) {
this.userId = Meteor.userId();
test.notEqual(this.userId, null);
test.notEqual(this.userId, this.otherUserId);
// Can't update fields other than profile.
await Meteor.users
.updateAsync(this.userId, {
$set: { disallowed: true, "profile.updated": 42 },
})
.catch((err) => {
test.isTrue(err);
test.equal(err.error, 403);
test.isFalse(
Object.prototype.hasOwnProperty.call(Meteor.user(), "disallowed")
);
test.isFalse(
Object.prototype.hasOwnProperty.call(
Meteor.user().profile,
"updated"
)
);
});
},
async function (test, expect) {
// Can't update another user.
await Meteor.users
.updateAsync(this.otherUserId, { $set: { "profile.updated": 42 } })
.catch(
expect((err) => {
test.isTrue(err);
test.equal(err.error, 403);
})
);
},
async function (test, expect) {
// Can't update using a non-ID selector. (This one is thrown client-side.)
await test.throwsAsync(async () => await Meteor.users.updateAsync(
{ username: this.username }, { $set: { 'profile.updated': 42 } }
));
test.isFalse(Object.prototype.hasOwnProperty.call(Meteor.user().profile, 'updated'));
},
async function (test) {
// Can update own profile using ID.
await Meteor.users.updateAsync(
this.userId, { $set: { 'profile.updated': 42 } },
);
test.equal(42, Meteor.user().profile.updated);
},
logoutStep
]);
testAsyncMulti("passwords - tokens", [
function (test, expect) {
// setup
this.username = Random.id();
this.password = 'password';
Accounts.createUser(
{ username: this.username, password: this.password },
loggedInAs(this.username, test, expect));
},
function (test, expect) {
// we can't login with an invalid token
const expectLoginError = expect(err => test.isTrue(err));
Meteor.loginWithToken('invalid', expectLoginError);
},
function (test, expect) {
// we can login with a valid token
const expectLoginOK = expect(err => test.isFalse(err));
Meteor.loginWithToken(Accounts._storedLoginToken(), expectLoginOK);
},
function (test, expect) {
// test logging out invalidates our token
const expectLoginError = expect(err => test.isTrue(err));
const token = Accounts._storedLoginToken();
test.isTrue(token);
Meteor.logout(() => Meteor.loginWithToken(token, expectLoginError));
},
function (test, expect) {
// Test that login tokens get expired. We should get logged out when a
// token expires, and not be able to log in again with the same token.
const expectNoError = expect(err => {
test.isFalse(err);
});
Meteor.loginWithPassword(this.username, this.password, error => {
this.token = Accounts._storedLoginToken();
test.isTrue(this.token);
expectNoError(error);
Accounts.connection.call("expireTokens");
});
},
waitForLoggedOutStep,
function (test, expect) {
const token = Accounts._storedLoginToken();
test.isFalse(token);
},
function (test, expect) {
// Test that once expireTokens is finished, we can't login again with our
// previous token.
Meteor.loginWithToken(this.token, expect((err, result) => {
test.isTrue(err);
test.equal(Meteor.userId(), null);
}));
},
logoutStep,
function (test, expect) {
// Test that Meteor.logoutOtherClients logs out a second
// authenticated connection while leaving Accounts.connection
// logged in.
const secondConn = DDP.connect(Meteor.absoluteUrl());
let token;
const expectSecondConnLoggedOut =
expect((err, result) => test.isTrue(err));
const expectAccountsConnLoggedIn =
expect((err, result) => test.isFalse(err));
const expectSecondConnLoggedIn = expect((err, result) => {
test.equal(result.token, token);
test.isFalse(err);
Meteor.logoutOtherClients(err => {
test.isFalse(err);
secondConn.call('login', { resume: token },
expectSecondConnLoggedOut);
Accounts.connection.call('login', {
resume: Accounts._storedLoginToken()
}, expectAccountsConnLoggedIn);
});
});
Meteor.loginWithPassword(
this.username,
this.password,
expect(err => {
test.isFalse(err);
token = Accounts._storedLoginToken();
test.isTrue(token);
secondConn.call('login', { resume: token },
expectSecondConnLoggedIn);
})
);
},
logoutStep,
]);
testAsyncMulti("passwords - validateLoginAttempt", [
function (test, expect) {
this.username = Random.id();
this.password = "password";
Accounts.createUser(
{ username: this.username, password: this.password },
loggedInAs(this.username, test, expect));
},
logoutStep,
invalidateLoginsStep,
function (test, expect) {
Meteor.loginWithPassword(
this.username,
this.password,
expect(error => {
test.isTrue(error);
test.equal(error.reason, "Login forbidden");
})
);
},
validateLoginsStep,
function (test, expect) {
Meteor.loginWithPassword(
"no such user",
"some password",
expect(error => {
test.isTrue(error);
test.equal(error.reason, 'User not found');
})
);
},
hideActualLoginErrorStep,
function (test, expect) {
Meteor.loginWithPassword(
"no such user",
"some password",
expect(error => {
test.isTrue(error);
test.equal(error.reason, 'hide actual error');
})
);
},
validateLoginsStep
]);
testAsyncMulti("passwords - server onLogin hook", [
function (test, expect) {
Meteor.call("testCaptureLogins", expect(error => test.isFalse(error)));
},
function (test, expect) {
this.username = Random.id();
this.password = "password";
Accounts.createUser(
{ username: this.username, password: this.password },
loggedInAs(this.username, test, expect));
},
function (test, expect) {
Meteor.call("testFetchCapturedLogins", expect((error, logins) => {
test.isFalse(error);
test.equal(logins.length, 1);
const login = logins[0];
test.isTrue(login.successful);
const { attempt } = login;
test.equal(attempt.type, "password");
test.isTrue(attempt.allowed);
test.equal(attempt.methodArguments[0].username, this.username);
}));
}
]);
testAsyncMulti("passwords - client onLogin hook", [
function (test, expect) {
this.username = Random.id();
this.password = "password";
this.attempt = false;
this.onLogin = Accounts.onLogin(attempt => {
this.attempt = true;
});
Accounts.createUser(
{ username: this.username, password: this.password },
loggedInAs(this.username, test, expect));
},
function (test, expect) {
this.onLogin.stop();
test.isTrue(this.attempt);
expect(() => ({}))();
}
]);
testAsyncMulti("passwords - server onLogout hook", [
function (test, expect) {
Meteor.call("testCaptureLogouts", expect(error => test.isFalse(error)));
},
function (test, expect) {
this.username = Random.id();
this.password = "password";
Accounts.createUser(
{ username: this.username, password: this.password },
loggedInAs(this.username, test, expect));
},
logoutStep,
function (test, expect) {
Meteor.call("testFetchCapturedLogouts", expect((error, logouts) => {
test.isFalse(error);
test.equal(logouts.length, 1);
const logout = logouts[0];
test.isTrue(logout.successful);
}));
}
]);
testAsyncMulti("passwords - client onLogout hook", [
function (test, expect) {
this.username = Random.id();
this.password = "password";
this.attempt = false;
this.onLogout = Accounts.onLogout(() => this.logoutSuccess = true);
Accounts.createUser(
{ username: this.username, password: this.password },
loggedInAs(this.username, test, expect));
},
logoutStep,
function (test, expect) {
test.isTrue(this.logoutSuccess);
expect(function () {
})();
}
]);
testAsyncMulti("passwords - server onLoginFailure hook", [
function (test, expect) {
this.username = Random.id();
this.password = "password";
Accounts.createUser(
{ username: this.username, password: this.password },
loggedInAs(this.username, test, expect));
},
logoutStep,
function (test, expect) {
Meteor.call("testCaptureLogins", expect(error => test.isFalse(error)));
},
function (test, expect) {
Meteor.loginWithPassword(
this.username,
"incorrect",
expect(error => test.isTrue(error))
);
},
function (test, expect) {
Meteor.call("testFetchCapturedLogins", expect((error, logins) => {
test.isFalse(error);
test.equal(logins.length, 1);
const login = logins[0];
test.isFalse(login.successful);
const { attempt } = login;
test.equal(attempt.type, "password");
test.isFalse(attempt.allowed);
test.equal(attempt.error.reason, "Incorrect password");
}));
},
function (test, expect) {
Meteor.call("testCaptureLogins", expect(error => test.isFalse(error)));
},
function (test, expect) {
Meteor.loginWithPassword(
"no such user",
"incorrect",
expect(error => test.isTrue(error))
);
},
function (test, expect) {
Meteor.call("testFetchCapturedLogins", expect((error, logins) => {
test.isFalse(error);
test.equal(logins.length, 1);
const login = logins[0];
test.isFalse(login.successful);
const { attempt } = login;
test.equal(attempt.type, "password");
test.isFalse(attempt.allowed);
test.equal(attempt.error.reason, "User not found");
}));
}
]);
testAsyncMulti("passwords - client onLoginFailure hook", [
function (test, expect) {
this.username = Random.id();
this.password = "password";
this.attempt = false;
this.onLoginFailure = Accounts.onLoginFailure(() => this.attempt = true);
Accounts.createUser(
{ username: this.username, password: this.password },
loggedInAs(this.username, test, expect));
},
logoutStep,
function (test, expect) {
Meteor.call("testCaptureLogins", expect(error => test.isFalse(error)));
},
function (test, expect) {
Meteor.loginWithPassword(
this.username,
"incorrect",
expect(error => test.isTrue(error))
);
},
function (test, expect) {
this.onLoginFailure.stop();
test.isTrue(this.attempt);
expect(() => ({}))();
}
]);
})();
if (Meteor.isServer) (() => {
Tinytest.add('passwords - setup more than one onCreateUserHook', test => {
test.throws(() => Accounts.onCreateUser(() => ({})));
});
Tinytest.addAsync('passwords - createUser hooks', async test => {
const username = Random.id();
// should fail the new user validators
await test.throwsAsync(
async () =>
await Accounts.createUser({ username: username, profile: { invalid: true } }));
const userId = await Accounts
.createUser({ username: username, testOnCreateUserHook: true });
test.isTrue(userId);
const user = await Meteor.users.findOneAsync(userId);
test.equal(user.profile.touchedByOnCreateUser, true);
});
Tinytest.addAsync(
'passwords - setPassword',
async test => {
const username = Random.id();
const email = `${ username }-intercept@example.com`;
const userId = await Accounts.createUser({ username: username, email: email });
let user = await Meteor.users.findOneAsync(userId);
// no services yet.
test.equal(user.services.password, undefined);
// set a new password.
await Accounts.setPasswordAsync(userId, 'new password');
user = await Meteor.users.findOneAsync(userId);
const oldSaltedHash = user.services.password.bcrypt;
test.isTrue(oldSaltedHash);
// Send a reset password email (setting a reset token) and insert a login
// token.
await Accounts.sendResetPasswordEmail(userId, email);
await Accounts._insertLoginToken(userId, Accounts._generateStampedLoginToken());
const user2 = await Meteor.users.findOneAsync(userId)
test.isTrue(user2.services.password.reset);
test.isTrue(user2.services.resume.loginTokens);
// reset with the same password, see we get a different salted hash
await Accounts.setPasswordAsync(userId, 'new password', { logout: false });
user = await Meteor.users.findOneAsync(userId);
const newSaltedHash = user.services.password.bcrypt;
test.isTrue(newSaltedHash);
test.notEqual(oldSaltedHash, newSaltedHash);
// No more reset token.
const user3 = await Meteor.users.findOneAsync(userId)
test.isFalse(user3.services.password.reset);
// But loginTokens are still here since we did logout: false.
test.isTrue(user3.services.resume.loginTokens);
// reset again, see that the login tokens are gone.
await Accounts.setPasswordAsync(userId, 'new password');
user = await Meteor.users.findOneAsync(userId);
const newerSaltedHash = user.services.password.bcrypt;
test.isTrue(newerSaltedHash);
test.notEqual(oldSaltedHash, newerSaltedHash);
test.notEqual(newSaltedHash, newerSaltedHash);
// No more tokens.
const user4 = await Meteor.users.findOneAsync(userId)
test.isFalse(user4.services.password.reset);
test.isFalse(user4.services.resume.loginTokens);
// cleanup
await Meteor.users.removeAsync(userId);
});
// This test properly belongs in accounts-base/accounts_tests.js, but
// this is where the tests that actually log in are.
Tinytest.addAsync('accounts - userAsync() out of context', async test => {
await test.throwsAsync(
async () =>
await Meteor.userAsync()
);
await Meteor.users.removeAsync({});
});
// XXX would be nice to test
// Accounts.config({forbidClientAccountCreation: true})
//TODO[FIBERS] Continue later
Tinytest.addAsync(
'passwords - login token observes get cleaned up',
async (test) => {
const username = Random.id();
const id = await Accounts.createUser({
username: username,
password: hashPasswordWithSha('password')
});
const {
clientConn, serverConn
} = await makeTestConnAsync(test)
serverConn.onClose(() => {
if (Accounts._getUserObserve(serverConn.id) !== undefined)
test.fail('observe should be gone');
})
const result = await clientConn.callAsync('login', {
user: { username: username },
password: hashPasswordWithSha('password')
});
test.isTrue(result);
const token = Accounts._getAccountData(serverConn.id, 'loginToken');
test.isTrue(token)
test.isTrue(Accounts._getUserObserve(serverConn.id));
try {
const r =
await simplePollAsync(() => !!Accounts._getUserObserve(serverConn.id))
test.isTrue(Accounts._getUserObserve(serverConn.id));
clientConn.disconnect()
serverConn.close()
} catch (e) {
test.fail(
`timed out waiting for user observe for connection ${ serverConn.id }`
);
}
}
);
Tinytest.addAsync(
"passwords - reset password doesn't work if email changed after email sent",
async test => {
const username = Random.id() + 'reset-password-doesnt-work-if-email-changed'
const email = `${ username }-intercept@example.com`;
const userId = await Accounts.createUser({
username: username,
email: email,
password: hashPasswordWithSha("old-password")
});
const user = await Meteor.users.findOneAsync(userId);
await Accounts.sendResetPasswordEmail(userId, email);
const [resetPasswordEmailOptions] =
await Meteor.callAsync("getInterceptedEmails", email);
const re = new RegExp(`${Meteor.absoluteUrl()}#/reset-password/(\\S*)`);
const match = resetPasswordEmailOptions.text.match(re);
test.isTrue(match);
const resetPasswordToken = match[1];
const newEmail = `${ Random.id() }-new@example.com`;
await Meteor.users.updateAsync(userId, { $set: { "emails.0.address": newEmail } });
await test.throwsAsync(
async () =>
await Meteor.callAsync("resetPassword", resetPasswordToken, hashPasswordWithSha("new-password")),
/Token has invalid email address/
);
Accounts._options.ambiguousErrorMessages = true;
await test.throwsAsync(
async () =>
await Meteor.callAsync(
"login",
{
user: { username: username },
password: hashPasswordWithSha("new-password")
}
),
/Something went wrong. Please check your credentials./);
});
Tinytest.addAsync(
'passwords - reset password should work when token is not expired',
async (test) => {
const username = Random.id() + 'reset-password-should-work-when-token-is-not-expired';
const email = `${ username }-intercept-reset@example.com`;
const userId = await Accounts.createUser({
username: username,
email: email,
password: hashPasswordWithSha("old-password")
});
const user = await Meteor.users.findOneAsync(userId);
await Accounts.sendResetPasswordEmail(userId, email);
const [resetPasswordEmailOptions] =
await Meteor.callAsync("getInterceptedEmails", email);
const re = new RegExp(`${Meteor.absoluteUrl()}#/reset-password/(\\S*)`);
const match = resetPasswordEmailOptions.text.match(re);
test.isTrue(match);
const resetPasswordToken = match[1];
const { clientConn } = await makeTestConnAsync(test)
test.isTrue(await clientConn.callAsync(
"resetPassword",
resetPasswordToken,
hashPasswordWithSha("new-password")
));
test.isTrue(await clientConn.callAsync("login", {
user: { username },
password: hashPasswordWithSha("new-password")
}));
});
Tinytest.addAsync(
'passwords - reset password should not work when token is expired',
async test => {
const username = Random.id();
const email = `${ username }-intercept@example.com`;
const userId = await Accounts.createUser({
username: username,
email: email,
password: hashPasswordWithSha("old-password")
});
const user = await Meteor.users.findOneAsync(userId);
await Accounts.sendResetPasswordEmail(userId, email);
const [resetPasswordEmailOptions] =
await Meteor.callAsync("getInterceptedEmails", email);
const re = new RegExp(`${Meteor.absoluteUrl()}#/reset-password/(\\S*)`);
const match = resetPasswordEmailOptions.text.match(re);
test.isTrue(match);
const resetPasswordToken = match[1];
await Meteor.users.updateAsync(userId, { $set: { "services.password.reset.when": new Date(Date.now() + -5 * 24 * 3600 * 1000) } });
try {
await Meteor.callAsync("resetPassword", resetPasswordToken, hashPasswordWithSha("new-password"))
} catch (e) {
test.throws(() => {
throw e;
})
}
Accounts._options.ambiguousErrorMessages = true;
await test.throwsAsync(
async () => await Meteor.callAsync(
"login",
{
user: { username: username },
password: hashPasswordWithSha("new-password")
}
),
/Something went wrong. Please check your credentials./);
});
Tinytest.addAsync('forgotPassword - different error messages returned depending' +
' on whether ambiguousErrorMessages flag is passed in Account.config',
async test => {
const username = Random.id();
const email = `${ Random.id() }-intercept@example.com`;
const randomEmail = `${ Random.id() }-Ada_intercept@some.com`;
const wrongOptions = { email: randomEmail }
const password = 'password';
const options = Accounts._options
await Accounts.createUser(
{
username: username,
email: email,
password: hashPasswordWithSha(password)
},
);
Accounts._options.ambiguousErrorMessages = true;
await test.throwsAsync(
async () => await Meteor.callAsync('forgotPassword', wrongOptions),
'Something went wrong. Please check your credentials'
);
Accounts._options.ambiguousErrorMessages = false;
await test.throwsAsync(
async () => await Meteor.callAsync('forgotPassword', wrongOptions),
/User not found/
);
// return accounts as it were
Accounts._options = options
});
Tinytest.addAsync(
'passwords - reset tokens with reasons get cleaned up',
async test => {
const email = `${ test.id }-intercept@example.com`;
const userId =
await Accounts.createUser(
{
email: email,
password: hashPasswordWithSha('password')
}
);
await Accounts.sendResetPasswordEmail(userId, email);
const user1 = await Meteor.users.findOneAsync(userId);
test.isTrue(!!user1.services.password.reset);
await Accounts._expirePasswordResetTokens(new Date(), userId);
const user2 = await Meteor.users.findOneAsync(userId);
test.isUndefined(user2.services.password.reset);
});
Tinytest.addAsync(
'passwords - reset tokens without reasons get cleaned up',
async test => {
const email = `${ test.id }-intercept@example.com`;
const userId =
await Accounts.createUser(
{
email: email,
password: hashPasswordWithSha('password')
}
);
await Accounts.sendResetPasswordEmail(userId, email);
await Meteor.users.updateAsync(
{ _id: userId },
{ $unset: { "services.password.reset.reason": 1 } }
);
const user1 = await Meteor.users.findOneAsync(userId);
test.isTrue(!!user1.services.password.reset);
test.isUndefined(user1.services.password.reset.reason);
await Accounts._expirePasswordResetTokens(new Date(), userId);
const user2 = await Meteor.users.findOneAsync(userId);
test.isUndefined(user2.services.password.reset);
});
Tinytest.addAsync(
'passwords - enroll password should work when token is not expired',
async test => {
const username = Random.id();
const email = `${ username }-intercept@example.com`;
const userId = await Accounts.createUser({
username: username,
email: email
});
const user = await Meteor.users.findOneAsync(userId);
await Accounts.sendEnrollmentEmail(userId, email);
const [enrollPasswordEmailOptions] =
await Meteor.callAsync("getInterceptedEmails", email);
const re = new RegExp(`${Meteor.absoluteUrl()}#/enroll-account/(\\S*)`);
const match = enrollPasswordEmailOptions.text.match(re);
test.isTrue(match);
const enrollPasswordToken = match[1];
const {
clientConn
} = await makeTestConnAsync(test)
test.isTrue(
await clientConn.callAsync(
"resetPassword",
enrollPasswordToken,
hashPasswordWithSha("new-password"))
);
test.isTrue(
await clientConn.callAsync("login", {
user: { username },
password: hashPasswordWithSha("new-password")
})
);
});
Tinytest.addAsync(
'passwords - enroll password should not work when token is expired',
async test => {
const username = Random.id();
const email = `${ username }-intercept@example.com`;
const userId = await Accounts.createUser({
username: username,
email: email
});
const user = await Meteor.users.findOneAsync(userId);
await Accounts.sendEnrollmentEmail(userId, email);
const [enrollPasswordEmailOptions] =
await Meteor.callAsync("getInterceptedEmails", email);
const re = new RegExp(`${Meteor.absoluteUrl()}#/enroll-account/(\\S*)`);
const match = enrollPasswordEmailOptions.text.match(re);
test.isTrue(match);
const enrollPasswordToken = match[1];
await Meteor.users.updateAsync(userId, { $set: { "services.password.enroll.when": new Date(Date.now() + -35 * 24 * 3600 * 1000) } });
await test.throwsAsync(
async () => await Meteor.callAsync("resetPassword", enrollPasswordToken, hashPasswordWithSha("new-password")),
/Token expired/
);
});
Tinytest.addAsync('passwords - enroll tokens get cleaned up',
async test => {
const email = `${ test.id }-intercept@example.com`;
const userId =
await Accounts.createUser({ email: email, password: hashPasswordWithSha('password') });
await Accounts.sendEnrollmentEmail(userId, email);
const user1 = await Meteor.users.findOneAsync(userId);
test.isTrue(!!user1.services.password.enroll);
await Accounts._expirePasswordEnrollTokens(new Date(), userId);
const user2 = await Meteor.users.findOneAsync(userId);
test.isUndefined(user2.services.password.enroll);
});
Tinytest.addAsync(
"passwords - enroll tokens don't get cleaned up when reset tokens are cleaned up",
async test => {
const email = `${ test.id }-intercept@example.com`;
const userId =
await Accounts.createUser({
email: email,
password: hashPasswordWithSha('password')
});
await Accounts.sendEnrollmentEmail(userId, email);
const user1 = await Meteor.users.findOneAsync(userId);
const enrollToken = user1.services.password.enroll;
test.isTrue(enrollToken);
await Accounts._expirePasswordResetTokens(new Date(), userId);
const user2 = await Meteor.users.findOneAsync(userId)
test.equal(enrollToken, user2.services.password.enroll);
}
)
Tinytest.addAsync(
"passwords - reset tokens don't get cleaned up when enroll tokens are cleaned up",
async test => {
const email = `${ test.id }-intercept@example.com`;
const userId =
await Accounts.createUser({ email: email, password: hashPasswordWithSha('password') });
await Accounts.sendResetPasswordEmail(userId, email);
const user1 = await Meteor.users.findOneAsync(userId);
const resetToken = user1.services.password.reset;
test.isTrue(resetToken);
await Accounts._expirePasswordEnrollTokens(new Date(), userId);
const user2 = await Meteor.users.findOneAsync(userId);
test.equal(resetToken, user2.services.password.reset);
}
)
// We should be able to change the username
Tinytest.addAsync("passwords - change username & findUserByUsername",
async test => {
const username = Random.id();
const ignoreFieldName = "profile";
const userId = await Accounts.createUser({
username,
[ignoreFieldName]: { name: 'foo' },
});
test.isTrue(userId);
const newUsername = Random.id();
await Accounts.setUsername(userId, newUsername);
const u1 = await Accounts._findUserByQuery({ id: userId })
test.equal(u1.username, newUsername);
// Test findUserByUsername as well while we're here
let user = await Accounts.findUserByUsername(newUsername);
test.equal(user._id, userId, 'userId - ignore');
test.isNotUndefined(user[ignoreFieldName], 'field - no ignore');
// Test default field selector
const options = Accounts._options;
Accounts._options = { defaultFieldSelector: { [ignoreFieldName]: 0 } };
user = await Accounts.findUserByUsername(newUsername);
test.equal(user.username, newUsername, 'username - default ignore');
test.isUndefined(user[ignoreFieldName], 'field - default ignore');
// Test default field selector over-ride
user = await Accounts.findUserByUsername(newUsername, {
fields: {
[ignoreFieldName]: 1
}
});
test.isUndefined(user.username, 'username - override');
test.isNotUndefined(user[ignoreFieldName], 'field - override');
Accounts._options = options;
});
Tinytest.addAsync("passwords - change username to a new one only differing " +
"in case", async test => {
const username = `${ Random.id() }user`;
const userId = await Accounts.createUser({
username: username.toUpperCase()
});
test.isTrue(userId);
const newUsername = username.toLowerCase();
await Accounts.setUsername(userId, newUsername);
const u1 = await Accounts._findUserByQuery({ id: userId })
test.equal(u1.username, newUsername);
});
// We should not be able to change the username to one that only
// differs in case from an existing one
Tinytest.addAsync("passwords - change username should fail when there are " +
"existing users with a username only differing in case",
async test => {
const username = `${ Random.id() }user`;
const usernameUpper = username.toUpperCase();
const userId1 = await Accounts.createUser({
username: username
});
const user2OriginalUsername = Random.id();
const userId2 = await Accounts.createUser({
username: user2OriginalUsername
});
test.isTrue(userId1);
test.isTrue(userId2);
Accounts._options.ambiguousErrorMessages = false;
await test.throwsAsync(
async () => await Accounts.setUsername(userId2, usernameUpper),
/Username already exists/
);
const u2 = await Accounts._findUserByQuery({ id: userId2 })
test.equal(u2.username, user2OriginalUsername);
});
Tinytest.addAsync("passwords - add email & findUserByEmail",
async test => {
const origEmail = `${ Random.id() }@turing.com`;
const username = Random.id();
const ignoreFieldName = "profile";
const userId = await Accounts.createUser({
email: origEmail,
username,
[ignoreFieldName]: { name: 'foo' },
});
const newEmail = `${ Random.id() }@turing.com`;
await Accounts.addEmailAsync(userId, newEmail);
const thirdEmail = `${ Random.id() }@turing.com`;
await Accounts.addEmailAsync(userId, thirdEmail, true);
const u1 = await Accounts._findUserByQuery({ id: userId })
test.equal(u1.emails, [
{ address: origEmail, verified: false },
{ address: newEmail, verified: false },
{ address: thirdEmail, verified: true }
]);
// Test findUserByEmail as well while we're here
let user = await Accounts.findUserByEmail(origEmail);
test.equal(user._id, userId);
test.isNotUndefined(user[ignoreFieldName], 'field - no ignore');
// Test default field selector
const options = Accounts._options;
Accounts._options = { defaultFieldSelector: { [ignoreFieldName]: 0 } };
user = await Accounts.findUserByEmail(origEmail);
test.equal(user.username, username, 'username - default ignore');
test.isUndefined(user[ignoreFieldName], 'field - default ignore');
// Test default field selector over-ride
user = await Accounts.findUserByEmail(origEmail, {
fields: {
[ignoreFieldName]: 1
}
});
test.equal(user._id, userId, 'userId - override');
test.isUndefined(user.username, 'username - override');
test.isNotUndefined(user[ignoreFieldName], 'field - override');
Accounts._options = options;
});
Tinytest.addAsync("passwords - add email when user has not an existing email",
async test => {
const userId = await Accounts.createUser({
username: `user${ Random.id() }`
});
const newEmail = `${ Random.id() }@turing.com`;
await Accounts.addEmailAsync(userId, newEmail);
const u1 = await Accounts._findUserByQuery({ id: userId })
test.equal(u1.emails, [
{ address: newEmail, verified: false },
]);
});
Tinytest.addAsync("passwords - add email when the user has an existing email " +
"only differing in case",
async test => {
const origEmail = `${ Random.id() }@turing.com`;
const userId = await Accounts.createUser({
email: origEmail
});
const newEmail = `${ Random.id() }@turing.com`;
await Accounts.addEmailAsync(userId, newEmail);
const thirdEmail = origEmail.toUpperCase();
await Accounts.addEmailAsync(userId, thirdEmail, true);
const u1 = await Accounts._findUserByQuery({ id: userId })
test.equal(u1.emails, [
{ address: thirdEmail, verified: true },
{ address: newEmail, verified: false }
]);
});
Tinytest.addAsync("passwords - add email should fail when there is an existing " +
"user with an email only differing in case",
async test => {
const user1Email = `${ Random.id() }@turing.com`;
const userId1 = await Accounts.createUser({
email: user1Email
});
const user2Email = `${ Random.id() }@turing.com`;
const userId2 = await Accounts.createUser({
email: user2Email
});
const dupEmail = user1Email.toUpperCase();
await test.throwsAsync(
async () => await Accounts.addEmailAsync(userId2, dupEmail),
/Email already exists/
);
const u1 = await Accounts._findUserByQuery({ id: userId1 })
test.equal(u1.emails, [
{ address: user1Email, verified: false }
]);
const u2 = await Accounts._findUserByQuery({ id: userId2 })
test.equal(u2.emails, [
{ address: user2Email, verified: false }
]);
});
Tinytest.addAsync("accounts emails - replace email", async test => {
const origEmail = `originalemail@test.com`;
const userId = await Accounts.createUserAsync({
email: origEmail,
password: 'password'
});
const newEmail = `newemail@test.com`;
const u1 = await Accounts._findUserByQuery({ id: userId })
test.equal(u1.emails, [
{ address: origEmail, verified: false }
]);
await Accounts.replaceEmailAsync(userId, origEmail, newEmail);
const u2 = await Accounts._findUserByQuery({ id: userId })
test.equal(u2.emails, [
{ address: newEmail, verified: false }
]);
})
Tinytest.addAsync("passwords - remove email",
async test => {
const origEmail = `${ Random.id() }@turing.com`;
const userId = await Accounts.createUser({
email: origEmail
});
const newEmail = `${ Random.id() }@turing.com`;
await Accounts.addEmailAsync(userId, newEmail);
const thirdEmail = `${ Random.id() }@turing.com`;
await Accounts.addEmailAsync(userId, thirdEmail, true);
const u1 = await Accounts._findUserByQuery({ id: userId })
test.equal(u1.emails, [
{ address: origEmail, verified: false },
{ address: newEmail, verified: false },
{ address: thirdEmail, verified: true }
]);
await Accounts.removeEmail(userId, newEmail);
const u2 = await Accounts._findUserByQuery({ id: userId })
test.equal(u2.emails, [
{ address: origEmail, verified: false },
{ address: thirdEmail, verified: true }
]);
await Accounts.removeEmail(userId, origEmail);
const u3 = await Accounts._findUserByQuery({ id: userId })
test.equal(u3.emails, [
{ address: thirdEmail, verified: true }
]);
});
const getUserHashRounds = user =>
Number(user.services.password.bcrypt.substring(4, 6));
testAsyncMulti("passwords - allow custom bcrypt rounds",[
async function (test) {
// Verify that a bcrypt hash generated for a new account uses the
let username = Random.id();
this.password = hashPasswordWithSha('abc123');
this.userId1 = await Accounts.createUser({ username, password: this.password });
this.user1 = await Meteor.users.findOneAsync(this.userId1);
let rounds = getUserHashRounds(this.user1);
test.equal(rounds, Accounts._bcryptRounds());
// When a custom number of bcrypt rounds is set via Accounts.config,
// and an account was already created using the default number of rounds,
// make sure that a new hash is created (and stored) using the new number
// of rounds, the next time the password is checked.
this.customRounds = 11;
Accounts._options.bcryptRounds = this.customRounds;
await Accounts._checkPasswordAsync(this.user1, this.password);
},
async function(test) {
const defaultRounds = Accounts._bcryptRounds();
let rounds;
let username;
let resolve;
const promise = new Promise(res => resolve = res);
Meteor.setTimeout(async () => {
this.user1 = await Meteor.users.findOneAsync(this.userId1);
rounds = getUserHashRounds(this.user1);
test.equal(rounds, this.customRounds);
// When a custom number of bcrypt rounds is set, make sure it's
// used for new bcrypt password hashes.
username = Random.id();
const userId2 = await Accounts.createUser({ username, password: this.password });
const user2 = await Meteor.users.findOneAsync(userId2);
rounds = getUserHashRounds(user2);
test.equal(rounds, this.customRounds);
// Cleanup
Accounts._options.bcryptRounds = defaultRounds;
await Meteor.users.removeAsync(this.userId1);
await Meteor.users.removeAsync(userId2);
resolve();
}, 5000);
return promise;
}
]); // default number of rounds.
Tinytest.addAsync('passwords - extra params in email urls',
async (test) => {
const username = Random.id();
const email = `${ username }-intercept@example.com`;
const userId = await Accounts.createUser({
username: username,
email: email
});
const extraParams = { test: 'success' };
await Accounts.sendEnrollmentEmail(userId, email, null, extraParams);
const [enrollPasswordEmailOptions] =
await Meteor.callAsync("getInterceptedEmails", email);
const re = new RegExp(`${Meteor.absoluteUrl()}(\\S*)`);
const match = enrollPasswordEmailOptions.text.match(re);
const url = new URL(match)
test.equal(url.searchParams.get('test'), extraParams.test);
});
Tinytest.addAsync('passwords - createUserAsync', async test => {
const username = Random.id();
const email = `${username}-intercept@example.com`;
const password = 'password';
const userId = await Accounts.createUserAsync({
username: username,
email: email,
password: password
});
test.isTrue(userId, 'User ID should be returned');
const user = await Meteor.users.findOneAsync(userId);
test.equal(user.username, username, 'Username should match');
test.equal(user.emails[0].address, email, 'Email should match');
Accounts.config({
ambiguousErrorMessages: false,
})
await test.throwsAsync(async () => {
await Accounts.createUserAsync({
username: username,
email: email,
password: password
});
}, 'already exists');
});
Tinytest.addAsync('passwords - send email functions', async test => {
// Create a user with an unverified email
const username = Random.id();
const email = `${username}-intercept@example.com`;
const password = 'password';
const userId = await Accounts.createUserAsync({
username: username,
email: email,
password: password
});
test.isTrue(userId, 'User ID should be returned');
// Mock Email.sendAsync to track if it was called
const originalSendAsync = Email.sendAsync;
let emailSent = 0;
Email.sendAsync = async (options) => {
emailSent++;
return originalSendAsync(options);
};
try {
// Test sendVerificationEmail
const verificationResult = await Accounts.sendVerificationEmail(userId, email);
// Verify the result contains expected properties
test.isTrue(verificationResult, 'Result should be returned for verification email');
test.equal(verificationResult.email, email, 'Email in verification result should match');
test.isTrue(verificationResult.user, 'User object should be in verification result');
test.isTrue(verificationResult.token, 'Token should be in verification result');
test.isTrue(verificationResult.url, 'URL should be in verification result');
test.isTrue(verificationResult.options, 'Email options should be in verification result');
// Test sendEnrollmentEmail
const enrollmentResult = await Accounts.sendEnrollmentEmail(userId, email);
// Verify the result contains expected properties
test.isTrue(enrollmentResult, 'Result should be returned for enrollment email');
test.equal(enrollmentResult.email, email, 'Email in enrollment result should match');
test.isTrue(enrollmentResult.user, 'User object should be in enrollment result');
test.isTrue(enrollmentResult.token, 'Token should be in enrollment result');
test.isTrue(enrollmentResult.url, 'URL should be in enrollment result');
test.isTrue(enrollmentResult.options, 'Email options should be in enrollment result');
// Test sendResetPasswordEmail
const resetResult = await Accounts.sendResetPasswordEmail(userId, email);
// Verify the result contains expected properties
test.isTrue(resetResult, 'Result should be returned for reset password email');
test.equal(resetResult.email, email, 'Email in reset result should match');
test.isTrue(resetResult.user, 'User object should be in reset result');
test.isTrue(resetResult.token, 'Token should be in reset result');
test.isTrue(resetResult.url, 'URL should be in reset result');
test.isTrue(resetResult.options, 'Email options should be in reset result');
// Verify Email.sendAsync was called for all three emails
test.equal(emailSent, 3, 'Email.sendAsync should have been called three times');
// Get the intercepted emails
const interceptedEmails = await Meteor.callAsync("getInterceptedEmails", email);
test.equal(interceptedEmails.length, 3, 'Three emails should have been intercepted');
// Verify the verification email content
const verificationEmailOptions = interceptedEmails[0];
test.isTrue(verificationEmailOptions, 'Verification email should have been intercepted');
const verificationRe = new RegExp(`${Meteor.absoluteUrl()}#/verify-email/(\\S*)`);
const verificationMatch = verificationEmailOptions.text.match(verificationRe);
test.isTrue(verificationMatch, 'Verification email should contain verification URL');
const verificationTokenFromUrl = verificationMatch[1];
test.isTrue(verificationResult.url.includes(verificationTokenFromUrl), 'Verification URL in result should contain the token');
// Verify the enrollment email content
const enrollmentEmailOptions = interceptedEmails[1];
test.isTrue(enrollmentEmailOptions, 'Enrollment email should have been intercepted');
const enrollmentRe = new RegExp(`${Meteor.absoluteUrl()}#/enroll-account/(\\S*)`);
const enrollmentMatch = enrollmentEmailOptions.text.match(enrollmentRe);
test.isTrue(enrollmentMatch, 'Enrollment email should contain enrollment URL');
const enrollmentTokenFromUrl = enrollmentMatch[1];
test.isTrue(enrollmentResult.url.includes(enrollmentTokenFromUrl), 'Enrollment URL in result should contain the token');
// Verify the reset password email content
const resetEmailOptions = interceptedEmails[2];
test.isTrue(resetEmailOptions, 'Reset password email should have been intercepted');
const resetRe = new RegExp(`${Meteor.absoluteUrl()}#/reset-password/(\\S*)`);
const resetMatch = resetEmailOptions.text.match(resetRe);
test.isTrue(resetMatch, 'Reset password email should contain reset URL');
const resetTokenFromUrl = resetMatch[1];
test.isTrue(resetResult.url.includes(resetTokenFromUrl), 'Reset URL in result should contain the token');
// Verify email headers and from address for all emails
for (const emailOptions of interceptedEmails) {
test.equal(emailOptions.from, 'test@meteor.com', 'From address should match');
test.equal(emailOptions.headers['My-Custom-Header'], 'Cool', 'Custom header should be present');
}
} finally {
// Restore the original Email.sendAsync
Email.sendAsync = originalSendAsync;
}
});
})();