diff --git a/packages/accounts-base/accounts_client.js b/packages/accounts-base/accounts_client.js index 842e927ad9..6f3314ad04 100644 --- a/packages/accounts-base/accounts_client.js +++ b/packages/accounts-base/accounts_client.js @@ -119,18 +119,21 @@ export class AccountsClient extends AccountsCommon { */ logout(callback) { this._loggingOut.set(true); - this.connection.apply('logout', [], { + + this.connection.applyAsync('logout', [], { + // TODO[FIBERS]: Look this { wait: true } later. wait: true - }, (error, result) => { - this._loggingOut.set(false); - this._loginCallbacksCalled = false; - if (error) { - callback && callback(error); - } else { + }) + .then((result) => { + this._loggingOut.set(false); + this._loginCallbacksCalled = false; this.makeClientLoggedOut(); callback && callback(); - } - }); + }) + .catch((e) => { + this._loggingOut.set(false); + callback && callback(e); + }); } /** diff --git a/packages/accounts-base/accounts_client_tests.js b/packages/accounts-base/accounts_client_tests.js index 880a71e4fe..5558884273 100644 --- a/packages/accounts-base/accounts_client_tests.js +++ b/packages/accounts-base/accounts_client_tests.js @@ -36,9 +36,9 @@ const createUserAndLogout = (test, done, nextTests) => { }, }, () => { - Meteor.logout(() => { + Meteor.logout(async () => { // Make sure we're logged out - test.isFalse(Meteor.user()); + test.isFalse(await Meteor.userAsync()); // Handle next tests nextTests(test, done); }); @@ -245,13 +245,13 @@ Tinytest.addAsync( ); -Tinytest.addAsync( + Tinytest.addAsync( 'accounts-2fa - Meteor.loginWithPasswordAnd2faCode() fails with invalid code', (test, done) => { createUserAndLogout(test, done, () => { forceEnableUser2fa(() => { - Meteor.loginWithPasswordAnd2faCode(username, password, 'ABC', e => { - test.isFalse(Meteor.user()); + Meteor.loginWithPasswordAnd2faCode(username, password, 'ABC', async e => { + test.isFalse(await Meteor.user()); test.equal(e.reason, 'Invalid 2FA code'); removeTestUser(done); }); diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 2fd0a6d41b..7b28f3bd22 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -14,6 +14,7 @@ const NonEmptyString = Match.Where(x => { return x.length > 0; }); + /** * @summary Constructor for the `Accounts` namespace on the server. * @locus Server @@ -71,8 +72,6 @@ export class AccountsServer extends AccountsCommon { // list of all registered handlers. this._loginHandlers = []; - - setupUsersCollection(this.users); setupDefaultLoginHandlers(this); setExpireTokensInterval(this); @@ -126,6 +125,10 @@ export class AccountsServer extends AccountsCommon { return currentInvocation.userId; } + async init() { + await setupUsersCollection(this.users); + } + /// /// LOGIN HOOKS /// @@ -259,11 +262,11 @@ export class AccountsServer extends AccountsCommon { }); }; - _successfulLogout(connection, userId) { + async _successfulLogout(connection, userId) { // don't fetch the user object unless there are some callbacks registered let user; - this._onLogoutHook.each(callback => { - if (!user && userId) user = this.users.findOne(userId, {fields: this._options.defaultFieldSelector}); + await this._onLogoutHook.forEachAsync(async callback => { + if (!user && userId) user = await this.users.findOne(userId, { fields: this._options.defaultFieldSelector }); callback({ user, connection }); return true; }); @@ -399,10 +402,10 @@ export class AccountsServer extends AccountsCommon { // indicates that the login token has already been inserted into the // database and doesn't need to be inserted again. (It's used by the // "resume" login handler). - _loginUser(methodInvocation, userId, stampedLoginToken) { + async _loginUser(methodInvocation, userId, stampedLoginToken) { if (! stampedLoginToken) { stampedLoginToken = this._generateStampedLoginToken(); - this._insertLoginToken(userId, stampedLoginToken); + await this._insertLoginToken(userId, stampedLoginToken); } // This order (and the avoidance of yields) is important to make @@ -473,12 +476,13 @@ export class AccountsServer extends AccountsCommon { this._validateLogin(methodInvocation.connection, attempt); if (attempt.allowed) { + const o = await this._loginUser( + methodInvocation, + result.userId, + result.stampedLoginToken + ) const ret = { - ...this._loginUser( - methodInvocation, - result.userId, - result.stampedLoginToken - ), + ...o, ...result.options }; ret.type = attempt.type; @@ -615,8 +619,8 @@ export class AccountsServer extends AccountsCommon { // Any connections associated with old-style unhashed tokens will be // in the process of becoming associated with hashed tokens and then // they'll get closed. - destroyToken(userId, loginToken) { - this.users.update(userId, { + async destroyToken(userId, loginToken) { + await this.users.update(userId, { $pull: { "services.resume.loginTokens": { $or: [ @@ -653,13 +657,13 @@ export class AccountsServer extends AccountsCommon { return await accounts._attemptLogin(this, "login", arguments, result); }; - methods.logout = function () { + methods.logout = async function () { const token = accounts._getLoginToken(this.connection.id); accounts._setLoginToken(this.userId, this.connection, null); if (token && this.userId) { - accounts.destroyToken(this.userId, token); + await accounts.destroyToken(this.userId, token); } - accounts._successfulLogout(this.connection, this.userId); + await accounts._successfulLogout(this.connection, this.userId); this.setUserId(null); }; @@ -671,8 +675,8 @@ export class AccountsServer extends AccountsCommon { // @returns Object // If successful, returns { token: , id: , // tokenExpires: }. - methods.getNewToken = function () { - const user = accounts.users.findOne(this.userId, { + methods.getNewToken = async function () { + const user = await accounts.users.findOne(this.userId, { fields: { "services.resume.loginTokens": 1 } }); if (! this.userId || ! user) { @@ -691,8 +695,8 @@ export class AccountsServer extends AccountsCommon { } const newStampedToken = accounts._generateStampedLoginToken(); newStampedToken.when = currentStampedToken.when; - accounts._insertLoginToken(this.userId, newStampedToken); - return accounts._loginUser(this, this.userId, newStampedToken); + await accounts._insertLoginToken(this.userId, newStampedToken); + return await accounts._loginUser(this, this.userId, newStampedToken); }; // Removes all tokens except the token associated with the current @@ -896,10 +900,10 @@ export class AccountsServer extends AccountsCommon { // Using $addToSet avoids getting an index error if another client // logging in simultaneously has already inserted the new hashed // token. - _insertHashedLoginToken(userId, hashedToken, query) { + async _insertHashedLoginToken(userId, hashedToken, query) { query = query ? { ...query } : {}; query._id = userId; - this.users.update(query, { + await this.users.update(query, { $addToSet: { "services.resume.loginTokens": hashedToken } @@ -907,14 +911,20 @@ export class AccountsServer extends AccountsCommon { }; // Exported for tests. - _insertLoginToken(userId, stampedToken, query) { - this._insertHashedLoginToken( + async _insertLoginToken(userId, stampedToken, query) { + await this._insertHashedLoginToken( userId, this._hashStampedToken(stampedToken), query ); }; + /** + * + * @param userId + * @private + * @returns {Promise} + */ _clearAllLoginTokens(userId) { this.users.update(userId, { $set: { @@ -972,7 +982,7 @@ export class AccountsServer extends AccountsCommon { // already -- in this case we just clean up the observe that we started). const myObserveNumber = ++this._nextUserObserveNumber; this._userObservesForConnections[connection.id] = myObserveNumber; - Meteor.defer(() => { + Meteor.defer(async () => { // If something else happened on this connection in the meantime (it got // closed, or another call to _setLoginToken happened), just do // nothing. We don't need to start an observe for an old connection or old @@ -985,7 +995,7 @@ export class AccountsServer extends AccountsCommon { // Because we upgrade unhashed login tokens to hashed tokens at // login time, sessions will only be logged in with a hashed // token. Thus we only need to observe hashed tokens here. - const observe = this.users.find({ + const observe = await this.users.find({ _id: userId, 'services.resume.loginTokens.hashedToken': newToken }, { fields: { _id: 1 } }).observeChanges({ @@ -1096,7 +1106,14 @@ export class AccountsServer extends AccountsCommon { // tests. oldestValidDate is simulate expiring tokens without waiting // for them to actually expire. userId is used by tests to only expire // tokens for the test user. - _expireTokens(oldestValidDate, userId) { + /** + * + * @param oldestValidDate + * @param userId + * @private + * @return {Promise} + */ + async _expireTokens(oldestValidDate, userId) { const tokenLifetimeMs = this._getTokenLifetimeMs(); // when calling from a test with extra arguments, you must specify both! @@ -1111,7 +1128,7 @@ export class AccountsServer extends AccountsCommon { // Backwards compatible with older versions of meteor that stored login token // timestamps as numbers. - this.users.update({ ...userFilter, + await this.users.update({ ...userFilter, $or: [ { "services.resume.loginTokens.when": { $lt: oldestValidDate } }, { "services.resume.loginTokens.when": { $lt: +oldestValidDate } } @@ -1148,7 +1165,7 @@ export class AccountsServer extends AccountsCommon { }; // Called by accounts-password - insertUserDoc(options, user) { + async insertUserDoc(options, user) { // - clone user document, to protect from modification // - add createdAt timestamp // - prepare an _id, so that you can modify other collections (eg @@ -1193,7 +1210,7 @@ export class AccountsServer extends AccountsCommon { let userId; try { - userId = this.users.insert(fullUser); + userId = await this.users.insert(fullUser); } catch (e) { // XXX string parsing sucks, maybe // https://jira.mongodb.org/browse/SERVER-3069 will get fixed one day @@ -1223,9 +1240,9 @@ export class AccountsServer extends AccountsCommon { /// CLEAN UP FOR `logoutOtherClients` /// - _deleteSavedTokensForUser(userId, tokensToDelete) { + async _deleteSavedTokensForUser(userId, tokensToDelete) { if (tokensToDelete) { - this.users.update(userId, { + await this.users.update(userId, { $unset: { "services.resume.haveLoginTokensToDelete": 1, "services.resume.loginTokensToDelete": 1 @@ -1244,16 +1261,23 @@ export class AccountsServer extends AccountsCommon { // shouldn't happen very often. We shouldn't put a delay here because // that would give a lot of power to an attacker with a stolen login // token and the ability to crash the server. - Meteor.startup(() => { - this.users.find({ + Meteor.startup(async () => { + await this.users.find({ "services.resume.haveLoginTokensToDelete": true - }, {fields: { + }, { + fields: { "services.resume.loginTokensToDelete": 1 - }}).forEach(user => { + } + }).forEach(user => { this._deleteSavedTokensForUser( user._id, user.services.resume.loginTokensToDelete - ); + ) + // We don't need to wait for this to complete. + .then(_ => _) + .catch(err => { + console.log(err); + }); }); }); }; @@ -1273,7 +1297,7 @@ export class AccountsServer extends AccountsCommon { // @returns {Object} Object with token and id keys, like the result // of the "login" method. // - updateOrCreateUserFromExternalService( + async updateOrCreateUserFromExternalService( serviceName, serviceData, options @@ -1308,13 +1332,11 @@ export class AccountsServer extends AccountsCommon { } else { selector[serviceIdKey] = serviceData.id; } - - let user = this.users.findOne(selector, {fields: this._options.defaultFieldSelector}); - + let user = await this.users.findOne(selector, {fields: this._options.defaultFieldSelector}); // Check to see if the developer has a custom way to find the user outside // of the general selectors above. if (!user && this._additionalFindUserOnExternalLogin) { - user = this._additionalFindUserOnExternalLogin({serviceName, serviceData, options}) + user = await this._additionalFindUserOnExternalLogin({serviceName, serviceData, options}) } // Before continuing, run user hook to see if we should continue @@ -1334,7 +1356,7 @@ export class AccountsServer extends AccountsCommon { } if (user) { - pinEncryptedFieldsToUser(serviceData, user._id); + await pinEncryptedFieldsToUser(serviceData, user._id); let setAttrs = {}; Object.keys(serviceData).forEach(key => @@ -1344,7 +1366,7 @@ export class AccountsServer extends AccountsCommon { // XXX Maybe we should re-use the selector above and notice if the update // touches nothing? setAttrs = { ...setAttrs, ...opts }; - this.users.update(user._id, { + await this.users.update(user._id, { $set: setAttrs }); @@ -1356,9 +1378,10 @@ export class AccountsServer extends AccountsCommon { // Create a new user with the service data. user = {services: {}}; user.services[serviceName] = serviceData; + const userId = await this.insertUserDoc(opts, user); return { type: serviceName, - userId: this.insertUserDoc(opts, user) + userId }; } }; @@ -1540,7 +1563,7 @@ const setupDefaultLoginHandlers = accounts => { }; // Login handler for resume tokens. -const defaultResumeLoginHandler = (accounts, options) => { +const defaultResumeLoginHandler = async (accounts, options) => { if (!options.resume) return undefined; @@ -1551,7 +1574,7 @@ const defaultResumeLoginHandler = (accounts, options) => { // First look for just the new-style hashed login token, to avoid // sending the unhashed token to the database in a query if we don't // need to. - let user = accounts.users.findOne( + let user = await accounts.users.findOne( {"services.resume.loginTokens.hashedToken": hashedToken}, {fields: {"services.resume.loginTokens.$": 1}}); @@ -1561,7 +1584,7 @@ const defaultResumeLoginHandler = (accounts, options) => { // the old-style token OR the new-style token, because another // client connection logging in simultaneously might have already // converted the token. - user = accounts.users.findOne({ + user = await accounts.users.findOne({ $or: [ {"services.resume.loginTokens.hashedToken": hashedToken}, {"services.resume.loginTokens.token": options.resume} @@ -1580,13 +1603,13 @@ const defaultResumeLoginHandler = (accounts, options) => { // {hashedToken, when} for a hashed token or {token, when} for an // unhashed token. let oldUnhashedStyleToken; - let token = user.services.resume.loginTokens.find(token => + let token = await user.services.resume.loginTokens.find(token => token.hashedToken === hashedToken ); if (token) { oldUnhashedStyleToken = false; } else { - token = user.services.resume.loginTokens.find(token => + token = await user.services.resume.loginTokens.find(token => token.token === options.resume ); oldUnhashedStyleToken = true; @@ -1606,7 +1629,7 @@ const defaultResumeLoginHandler = (accounts, options) => { // after we read it). Using $addToSet avoids getting an index // error if another client logging in simultaneously has already // inserted the new hashed token. - accounts.users.update( + await accounts.users.update( { _id: user._id, "services.resume.loginTokens.token": options.resume @@ -1622,7 +1645,7 @@ const defaultResumeLoginHandler = (accounts, options) => { // Remove the old token *after* adding the new, since otherwise // another client trying to login between our removing the old and // adding the new wouldn't find a token to login with. - accounts.users.update(user._id, { + await accounts.users.update(user._id, { $pull: { "services.resume.loginTokens": { "token": options.resume } } @@ -1638,49 +1661,50 @@ const defaultResumeLoginHandler = (accounts, options) => { }; }; -const expirePasswordToken = ( - accounts, - oldestValidDate, - tokenFilter, - userId -) => { - // boolean value used to determine if this method was called from enroll account workflow - let isEnroll = false; - const userFilter = userId ? {_id: userId} : {}; - // check if this method was called from enroll account workflow - if(tokenFilter['services.password.enroll.reason']) { - isEnroll = true; - } - let resetRangeOr = { - $or: [ - { "services.password.reset.when": { $lt: oldestValidDate } }, - { "services.password.reset.when": { $lt: +oldestValidDate } } - ] - }; - if(isEnroll) { - resetRangeOr = { +const expirePasswordToken = + async ( + accounts, + oldestValidDate, + tokenFilter, + userId + ) => { + // boolean value used to determine if this method was called from enroll account workflow + let isEnroll = false; + const userFilter = userId ? { _id: userId } : {}; + // check if this method was called from enroll account workflow + if (tokenFilter['services.password.enroll.reason']) { + isEnroll = true; + } + let resetRangeOr = { $or: [ - { "services.password.enroll.when": { $lt: oldestValidDate } }, - { "services.password.enroll.when": { $lt: +oldestValidDate } } + { "services.password.reset.when": { $lt: oldestValidDate } }, + { "services.password.reset.when": { $lt: +oldestValidDate } } ] }; - } - const expireFilter = { $and: [tokenFilter, resetRangeOr] }; - if(isEnroll) { - accounts.users.update({...userFilter, ...expireFilter}, { - $unset: { - "services.password.enroll": "" - } - }, { multi: true }); - } else { - accounts.users.update({...userFilter, ...expireFilter}, { - $unset: { - "services.password.reset": "" - } - }, { multi: true }); - } + if (isEnroll) { + resetRangeOr = { + $or: [ + { "services.password.enroll.when": { $lt: oldestValidDate } }, + { "services.password.enroll.when": { $lt: +oldestValidDate } } + ] + }; + } + const expireFilter = { $and: [tokenFilter, resetRangeOr] }; + if (isEnroll) { + await accounts.users.update({ ...userFilter, ...expireFilter }, { + $unset: { + "services.password.enroll": "" + } + }, { multi: true }); + } else { + await accounts.users.update({ ...userFilter, ...expireFilter }, { + $unset: { + "services.password.reset": "" + } + }, { multi: true }); + } -}; + }; const setExpireTokensInterval = accounts => { accounts.expireTokenInterval = Meteor.setInterval(() => { @@ -1747,7 +1771,7 @@ function defaultValidateNewUserHook(user) { } } -const setupUsersCollection = users => { +const setupUsersCollection = async users => { /// /// RESTRICTING WRITES TO USER OBJECTS /// @@ -1773,21 +1797,21 @@ const setupUsersCollection = users => { }); /// DEFAULT INDEXES ON USERS - users.createIndex('username', { unique: true, sparse: true }); - users.createIndex('emails.address', { unique: true, sparse: true }); - users.createIndex('services.resume.loginTokens.hashedToken', + await users.createIndex('username', { unique: true, sparse: true }); + await users.createIndex('emails.address', { unique: true, sparse: true }); + await users.createIndex('services.resume.loginTokens.hashedToken', { unique: true, sparse: true }); - users.createIndex('services.resume.loginTokens.token', + await users.createIndex('services.resume.loginTokens.token', { unique: true, sparse: true }); // For taking care of logoutOtherClients calls that crashed before the // tokens were deleted. - users.createIndex('services.resume.haveLoginTokensToDelete', + await users.createIndex('services.resume.haveLoginTokensToDelete', { sparse: true }); // For expiring login tokens - users.createIndex("services.resume.loginTokens.when", { sparse: true }); + await users.createIndex("services.resume.loginTokens.when", { sparse: true }); // For expiring password tokens - users.createIndex('services.password.reset.when', { sparse: true }); - users.createIndex('services.password.enroll.when', { sparse: true }); + await users.createIndex('services.password.reset.when', { sparse: true }); + await users.createIndex('services.password.enroll.when', { sparse: true }); }; diff --git a/packages/accounts-base/accounts_tests.js b/packages/accounts-base/accounts_tests.js index 797bd758f0..458c6ce589 100644 --- a/packages/accounts-base/accounts_tests.js +++ b/packages/accounts-base/accounts_tests.js @@ -4,8 +4,8 @@ import { Accounts } from 'meteor/accounts-base'; import { Random } from 'meteor/random'; Meteor.methods({ - getCurrentLoginToken: function () { - return Accounts._getLoginToken(this.connection.id); + getCurrentLoginToken: async function () { + return await Accounts._getLoginToken(this.connection.id); } }); @@ -13,18 +13,18 @@ Meteor.methods({ // *are* validated, but Accounts._options is global state which makes this hard // (impossible?) Tinytest.add( - 'accounts - config validates keys', - test => test.throws(() => Accounts.config({foo: "bar"})) + 'accounts - config - validates keys', + test => test.throws(() => Accounts.config({ foo: "bar" })) ); -Tinytest.add('accounts - config - token lifetime', test => { +Tinytest.addAsync('accounts - config - token lifetime', async test => { const { loginExpirationInDays } = Accounts._options; Accounts._options.loginExpirationInDays = 2; test.equal(Accounts._getTokenLifetimeMs(), 2 * 24 * 60 * 60 * 1000); Accounts._options.loginExpirationInDays = loginExpirationInDays; }); -Tinytest.add('accounts - config - unexpiring tokens', test => { +Tinytest.addAsync('accounts - config - unexpiring tokens', async test => { const { loginExpirationInDays } = Accounts._options; // When setting loginExpirationInDays to null in the global Accounts @@ -52,7 +52,7 @@ Tinytest.add('accounts - config - unexpiring tokens', test => { Accounts._options.loginExpirationInDays = loginExpirationInDays; }); -Tinytest.add('accounts - config - default token lifetime', test => { +Tinytest.addAsync('accounts - config - default token lifetime', async test => { const options = Accounts._options; Accounts._options = {}; test.equal( @@ -62,11 +62,11 @@ Tinytest.add('accounts - config - default token lifetime', test => { Accounts._options = options; }); -Tinytest.add('accounts - config - defaultFieldSelector', test => { +Tinytest.addAsync('accounts - config - defaultFieldSelector', async test => { const options = Accounts._options; Accounts._options = {}; - const setValue = {bigArray: 0}; - Accounts.config({defaultFieldSelector: setValue}); + const setValue = { bigArray: 0 }; + Accounts.config({ defaultFieldSelector: setValue }); test.equal(Accounts._options.defaultFieldSelector, setValue); Accounts._options = options; }); @@ -77,154 +77,51 @@ Accounts.validateNewUser(user => { return true; }); -Tinytest.add('accounts - validateNewUser gets passed user with _id', test => { - const newUserId = Accounts.updateOrCreateUserFromExternalService('foobook', {id: Random.id()}).userId; - test.isTrue(newUserId in idsInValidateNewUser); +Tinytest.addAsync('accounts - validateNewUser gets passed user with _id', async test => { + const { userId } = await Accounts.updateOrCreateUserFromExternalService('foobook', { id: Random.id() }); + test.isTrue(userId in idsInValidateNewUser); }); -Tinytest.add('accounts - updateOrCreateUserFromExternalService - Facebook', test => { - const facebookId = Random.id(); - - // create an account with facebook - const uid1 = Accounts.updateOrCreateUserFromExternalService( - 'facebook', {id: facebookId, monkey: 42}, {profile: {foo: 1}}).id; - const users1 = Meteor.users.find({"services.facebook.id": facebookId}).fetch(); - test.length(users1, 1); - test.equal(users1[0].profile.foo, 1); - test.equal(users1[0].services.facebook.monkey, 42); - - // create again with the same id, see that we get the same user. - // it should update services.facebook but not profile. - const uid2 = Accounts.updateOrCreateUserFromExternalService( - 'facebook', {id: facebookId, llama: 50}, - {profile: {foo: 1000, bar: 2}}).id; - test.equal(uid1, uid2); - const users2 = Meteor.users.find({"services.facebook.id": facebookId}).fetch(); - test.length(users2, 1); - test.equal(users2[0].profile.foo, 1); - test.equal(users2[0].profile.bar, undefined); - test.equal(users2[0].services.facebook.llama, 50); - // make sure we *don't* lose values not passed this call to - // updateOrCreateUserFromExternalService - test.equal(users2[0].services.facebook.monkey, 42); - - // cleanup - Meteor.users.remove(uid1); -}); - -Tinytest.add('accounts - updateOrCreateUserFromExternalService - Meteor Developer', test => { - const developerId = Random.id(); - const uid1 = Accounts.updateOrCreateUserFromExternalService( - 'meteor-developer', - { id: developerId, username: 'meteor-developer' }, - { profile: { name: 'meteor-developer' } } - ).id; - const users1 = Meteor.users.find({ 'services.meteor-developer.id': developerId }).fetch(); - test.length(users1, 1); - test.equal(users1[0].profile.name, 'meteor-developer'); - - const uid2 = Accounts.updateOrCreateUserFromExternalService( - 'meteor-developer', - { id: developerId, username: 'meteor-developer' }, - { profile: { name: 'meteor-developer', username: 'developer' } } - ).id; - test.equal(uid1, uid2); - const users2 = Meteor.users.find({ 'services.meteor-developer.id': developerId }).fetch(); - test.length(users2, 1); - test.equal(users1[0].profile.name, 'meteor-developer'); - test.equal(users1[0].profile.username, undefined); - - // cleanup - Meteor.users.remove(uid1); -}); - -Tinytest.add('accounts - updateOrCreateUserFromExternalService - Weibo', test => { - const weiboId1 = Random.id(); - const weiboId2 = Random.id(); - - // users that have different service ids get different users - const uid1 = Accounts.updateOrCreateUserFromExternalService( - 'weibo', {id: weiboId1}, {profile: {foo: 1}}).id; - const uid2 = Accounts.updateOrCreateUserFromExternalService( - 'weibo', {id: weiboId2}, {profile: {bar: 2}}).id; - test.equal(Meteor.users.find({"services.weibo.id": {$in: [weiboId1, weiboId2]}}).count(), 2); - test.equal(Meteor.users.findOne({"services.weibo.id": weiboId1}).profile.foo, 1); - test.equal(Meteor.users.findOne({"services.weibo.id": weiboId1}).emails, undefined); - test.equal(Meteor.users.findOne({"services.weibo.id": weiboId2}).profile.bar, 2); - test.equal(Meteor.users.findOne({"services.weibo.id": weiboId2}).emails, undefined); - - // cleanup - Meteor.users.remove(uid1); - Meteor.users.remove(uid2); -}); - -Tinytest.add('accounts - updateOrCreateUserFromExternalService - Twitter', test => { - const twitterIdOld = parseInt(Random.hexString(4), 16); - const twitterIdNew = ''+twitterIdOld; - - // create an account with twitter using the old ID format of integer - const uid1 = Accounts.updateOrCreateUserFromExternalService( - 'twitter', {id: twitterIdOld, monkey: 42}, {profile: {foo: 1}}).id; - const users1 = Meteor.users.find({"services.twitter.id": twitterIdOld}).fetch(); - test.length(users1, 1); - test.equal(users1[0].profile.foo, 1); - test.equal(users1[0].services.twitter.monkey, 42); - - // Update the account with the new ID format of string - // test that the existing user is found, and that the ID - // gets updated to a string value - const uid2 = Accounts.updateOrCreateUserFromExternalService( - 'twitter', {id: twitterIdNew, monkey: 42}, {profile: {foo: 1}}).id; - test.equal(uid1, uid2); - const users2 = Meteor.users.find({"services.twitter.id": twitterIdNew}).fetch(); - test.length(users2, 1); - - // cleanup - Meteor.users.remove(uid1); -}); - - -Tinytest.add('accounts - insertUserDoc username', test => { +Tinytest.addAsync('accounts - insertUserDoc username', async test => { const userIn = { username: Random.id() }; // user does not already exist. create a user object with fields set. - const userId = Accounts.insertUserDoc( - {profile: {name: 'Foo Bar'}}, + const userId = await Accounts.insertUserDoc( + { profile: { name: 'Foo Bar' } }, userIn ); - const userOut = Meteor.users.findOne(userId); - + const userOut = await Meteor.users.findOne(userId); test.equal(typeof userOut.createdAt, 'object'); test.equal(userOut.profile.name, 'Foo Bar'); test.equal(userOut.username, userIn.username); // run the hook again. now the user exists, so it throws an error. - test.throws( - () => Accounts.insertUserDoc({profile: {name: 'Foo Bar'}}, userIn), + await test.throwsAsync( + async () => await Accounts.insertUserDoc({ profile: { name: 'Foo Bar' } }, userIn), 'Username already exists.' ); // cleanup - Meteor.users.remove(userId); + await Meteor.users.remove(userId); }); -Tinytest.add('accounts - insertUserDoc email', test => { +Tinytest.addAsync('accounts - insertUserDoc email', async test => { const email1 = Random.id(); const email2 = Random.id(); const email3 = Random.id(); const userIn = { - emails: [{address: email1, verified: false}, - {address: email2, verified: true}] + emails: [{ address: email1, verified: false }, + { address: email2, verified: true }] }; // user does not already exist. create a user object with fields set. - const userId = Accounts.insertUserDoc( - {profile: {name: 'Foo Bar'}}, + const userId = await Accounts.insertUserDoc( + { profile: { name: 'Foo Bar' } }, userIn ); - const userOut = Meteor.users.findOne(userId); + const userOut = await Meteor.users.findOne(userId); test.equal(typeof userOut.createdAt, 'object'); test.equal(userOut.profile.name, 'Foo Bar'); @@ -232,43 +129,47 @@ Tinytest.add('accounts - insertUserDoc email', test => { // run the hook again with the exact same emails. // run the hook again. now the user exists, so it throws an error. - test.throws( - () => Accounts.insertUserDoc({profile: {name: 'Foo Bar'}}, userIn), + await test.throwsAsync( + async () => await Accounts.insertUserDoc({ profile: { name: 'Foo Bar' } }, userIn), 'Email already exists.' ); // now with only one of them. - test.throws(() => - Accounts.insertUserDoc({}, {emails: [{address: email1}]}), + await test.throwsAsync( + async () => + await Accounts.insertUserDoc({}, { emails: [{ address: email1 }] }), 'Email already exists.' ); - test.throws(() => - Accounts.insertUserDoc({}, {emails: [{address: email2}]}), + await test.throwsAsync( + async () => + await Accounts.insertUserDoc({}, { emails: [{ address: email2 }] }), 'Email already exists.' ); // a third email works. - const userId3 = Accounts.insertUserDoc( - {}, {emails: [{address: email3}]} + const userId3 = await Accounts.insertUserDoc( + {}, { emails: [{ address: email3 }] } ); - const user3 = Meteor.users.findOne(userId3); + const user3 = await Meteor.users.findOne(userId3); test.equal(typeof user3.createdAt, 'object'); // cleanup - Meteor.users.remove(userId); - Meteor.users.remove(userId3); + await Meteor.users.remove(userId); + await Meteor.users.remove(userId3); }); // More token expiration tests are in accounts-password -Tinytest.addAsync('accounts - expire numeric token', (test, onComplete) => { +Tinytest.addAsync('accounts - expire numeric token', async (test, onComplete) => { const userIn = { username: Random.id() }; - const userId = Accounts.insertUserDoc({ profile: { - name: 'Foo Bar' - } }, userIn); + const userId = await Accounts.insertUserDoc({ + profile: { + name: 'Foo Bar' + } + }, userIn); const date = new Date(new Date() - 5000); - Meteor.users.update(userId, { + await Meteor.users.update(userId, { $set: { "services.resume.loginTokens": [{ hashedToken: Random.id(), @@ -279,59 +180,64 @@ Tinytest.addAsync('accounts - expire numeric token', (test, onComplete) => { }] } }); - const observe = Meteor.users.find(userId).observe({ + const observe = await Meteor.users.find(userId).observe({ changed: newUser => { if (newUser.services && newUser.services.resume && - (!newUser.services.resume.loginTokens || + (!newUser.services.resume.loginTokens || newUser.services.resume.loginTokens.length === 0)) { observe.stop(); onComplete(); } } }); - Accounts._expireTokens(new Date(), userId); + await Accounts._expireTokens(new Date(), userId); }); // Login tokens used to be stored unhashed in the database. We want // to make sure users can still login after upgrading. -const insertUnhashedLoginToken = (userId, stampedToken) => { - Meteor.users.update( +const insertUnhashedLoginToken = async (userId, stampedToken) => { + await Meteor.users.update( userId, - {$push: {'services.resume.loginTokens': stampedToken}} + { $push: { 'services.resume.loginTokens': stampedToken } } ); }; -Tinytest.addAsync('accounts - login token', (test, onComplete) => { +Tinytest.addAsync('accounts - login token', async (test) => { // Test that we can login when the database contains a leftover // old style unhashed login token. - const userId1 = Accounts.insertUserDoc({}, {username: Random.id()}); + const userId1 = + await Accounts.insertUserDoc({}, { username: Random.id() }); const stampedToken1 = Accounts._generateStampedLoginToken(); - insertUnhashedLoginToken(userId1, stampedToken1); + await insertUnhashedLoginToken(userId1, stampedToken1); let connection = DDP.connect(Meteor.absoluteUrl()); - connection.call('login', {resume: stampedToken1.token}); + await connection.callAsync('login', { resume: stampedToken1.token }); connection.disconnect(); // Steal the unhashed token from the database and use it to login. // This is a sanity check so that when we *can't* login with a // stolen *hashed* token, we know it's not a problem with the test. - const userId2 = Accounts.insertUserDoc({}, {username: Random.id()}); - insertUnhashedLoginToken(userId2, Accounts._generateStampedLoginToken()); - const stolenToken1 = Meteor.users.findOne(userId2).services.resume.loginTokens[0].token; + const userId2 = + await Accounts.insertUserDoc({}, { username: Random.id() }); + await insertUnhashedLoginToken(userId2, Accounts._generateStampedLoginToken()); + const user2 = await Meteor.users.findOne(userId2); + const stolenToken1 = user2.services.resume.loginTokens[0].token; test.isTrue(stolenToken1); connection = DDP.connect(Meteor.absoluteUrl()); - connection.call('login', {resume: stolenToken1}); + await connection.callAsync('login', { resume: stolenToken1 }); connection.disconnect(); // Now do the same thing, this time with a stolen hashed token. - const userId3 = Accounts.insertUserDoc({}, {username: Random.id()}); - Accounts._insertLoginToken(userId3, Accounts._generateStampedLoginToken()); - const stolenToken2 = Meteor.users.findOne(userId3).services.resume.loginTokens[0].hashedToken; + const userId3 = + await Accounts.insertUserDoc({}, { username: Random.id() }); + await Accounts._insertLoginToken(userId3, Accounts._generateStampedLoginToken()); + const user3 = await Meteor.users.findOne(userId3); + const stolenToken2 = user3.services.resume.loginTokens[0].hashedToken; test.isTrue(stolenToken2); connection = DDP.connect(Meteor.absoluteUrl()); // evil plan foiled - test.throws( - () => connection.call('login', {resume: stolenToken2}), + await test.throwsAsync( + async () => await connection.callAsync('login', { resume: stolenToken2 }), /You\'ve been logged out by the server/ ); connection.disconnect(); @@ -339,24 +245,25 @@ Tinytest.addAsync('accounts - login token', (test, onComplete) => { // Old style unhashed tokens are replaced by hashed tokens when // encountered. This means that after someone logins once, the // old unhashed token is no longer available to be stolen. - const userId4 = Accounts.insertUserDoc({}, {username: Random.id()}); + const userId4 = + await Accounts.insertUserDoc({}, { username: Random.id() }); const stampedToken2 = Accounts._generateStampedLoginToken(); - insertUnhashedLoginToken(userId4, stampedToken2); + await insertUnhashedLoginToken(userId4, stampedToken2); connection = DDP.connect(Meteor.absoluteUrl()); - connection.call('login', {resume: stampedToken2.token}); + await connection.callAsync('login', { resume: stampedToken2.token }); connection.disconnect(); // The token is no longer available to be stolen. - const stolenToken3 = Meteor.users.findOne(userId4).services.resume.loginTokens[0].token; + const user4 = await Meteor.users.findOne(userId4); + const stolenToken3 = user4.services.resume.loginTokens[0].token; test.isFalse(stolenToken3); // After the upgrade, the client can still login with their original // unhashed login token. connection = DDP.connect(Meteor.absoluteUrl()); - connection.call('login', {resume: stampedToken2.token}); + await connection.callAsync('login', { resume: stampedToken2.token }); connection.disconnect(); - onComplete(); }); Tinytest.addAsync( @@ -380,59 +287,63 @@ Tinytest.addAsync( } ); -Tinytest.add('accounts - get new token', test => { +Tinytest.addAsync('accounts - get new token', async test => { // Test that the `getNewToken` method returns us a valid token, with // the same expiration as our original token. - const userId = Accounts.insertUserDoc({}, { username: Random.id() }); + const userId = await Accounts.insertUserDoc({}, { username: Random.id() }); const stampedToken = Accounts._generateStampedLoginToken(); - Accounts._insertLoginToken(userId, stampedToken); + await Accounts._insertLoginToken(userId, stampedToken); const conn = DDP.connect(Meteor.absoluteUrl()); - conn.call('login', { resume: stampedToken.token }); - test.equal(conn.call('getCurrentLoginToken'), - Accounts._hashLoginToken(stampedToken.token)); + await conn.callAsync('login', { resume: stampedToken.token }); + test.equal(await conn.callAsync('getCurrentLoginToken'), + Accounts._hashLoginToken(stampedToken.token)); - const newTokenResult = conn.call('getNewToken'); + const newTokenResult = await conn.callAsync('getNewToken'); test.equal(newTokenResult.tokenExpires, - Accounts._tokenExpiration(stampedToken.when)); - test.equal(conn.call('getCurrentLoginToken'), - Accounts._hashLoginToken(newTokenResult.token)); + Accounts._tokenExpiration(stampedToken.when)); + const token = await conn.callAsync('getCurrentLoginToken'); + test.equal(await conn.callAsync('getCurrentLoginToken'), + Accounts._hashLoginToken(newTokenResult.token)); conn.disconnect(); // A second connection should be able to log in with the new token // we got. const secondConn = DDP.connect(Meteor.absoluteUrl()); - secondConn.call('login', { resume: newTokenResult.token }); + await secondConn.callAsync('login', { resume: newTokenResult.token }); secondConn.disconnect(); } ); -Tinytest.addAsync('accounts - remove other tokens', (test, onComplete) => { +Tinytest.addAsync('accounts - remove other tokens', async (test) => { // Test that the `removeOtherTokens` method removes all tokens other // than the caller's token, thereby logging out and closing other // connections. - const userId = Accounts.insertUserDoc({}, { username: Random.id() }); + const userId = await Accounts.insertUserDoc({}, { username: Random.id() }); const stampedTokens = []; const conns = []; - for(let i = 0; i < 2; i++) { + for (let i = 0; i < 2; i++) { stampedTokens.push(Accounts._generateStampedLoginToken()); - Accounts._insertLoginToken(userId, stampedTokens[i]); + await Accounts._insertLoginToken(userId, stampedTokens[i]); const conn = DDP.connect(Meteor.absoluteUrl()); - conn.call('login', { resume: stampedTokens[i].token }); - test.equal(conn.call('getCurrentLoginToken'), - Accounts._hashLoginToken(stampedTokens[i].token)); + await conn.callAsync('login', { resume: stampedTokens[i].token }); + test.equal(await conn.callAsync('getCurrentLoginToken'), + Accounts._hashLoginToken(stampedTokens[i].token)); conns.push(conn); - }; + } + ; - conns[0].call('removeOtherTokens'); - simplePoll(() => { - const tokens = conns.map(conn => conn.call('getCurrentLoginToken')); - return ! tokens[1] && + await conns[0].callAsync('removeOtherTokens'); + simplePoll(async () => { + let tokens = []; + for (const conn of conns) { + tokens.push(await conn.callAsync('getCurrentLoginToken')); + } + return !tokens[1] && tokens[0] === Accounts._hashLoginToken(stampedTokens[0].token); }, () => { // success conns.forEach(conn => conn.disconnect()); - onComplete(); }, () => { // timed out throw new Error("accounts - remove other tokens timed out"); @@ -441,12 +352,12 @@ Tinytest.addAsync('accounts - remove other tokens', (test, onComplete) => { } ); -Tinytest.add( +Tinytest.addAsync( 'accounts - hook callbacks can access Meteor.userId()', - test => { - const userId = Accounts.insertUserDoc({}, { username: Random.id() }); + async test => { + const userId = await Accounts.insertUserDoc({}, { username: Random.id() }); const stampedToken = Accounts._generateStampedLoginToken(); - Accounts._insertLoginToken(userId, stampedToken); + await Accounts._insertLoginToken(userId, stampedToken); const validateStopper = Accounts.validateLoginAttempt(attempt => { test.equal(Meteor.userId(), validateAttemptExpectedUserId, "validateLoginAttempt"); @@ -468,20 +379,22 @@ Tinytest.add( // On a new connection, Meteor.userId() should be null until logged in. let validateAttemptExpectedUserId = null; const onLoginExpectedUserId = userId; - conn.call('login', { resume: stampedToken.token }); + await conn.callAsync('login', { resume: stampedToken.token }); // Now that the user is logged in on the connection, Meteor.userId() should // return that user. validateAttemptExpectedUserId = userId; - conn.call('login', { resume: stampedToken.token }); + await conn.callAsync('login', { resume: stampedToken.token }); // Trigger onLoginFailure callbacks const onLoginFailureExpectedUserId = userId; - test.throws(() => conn.call('login', { resume: "bogus" }), '403'); + await test.throwsAsync( + async () => + await conn.callAsync('login', { resume: "bogus" }), '403'); // Trigger onLogout callbacks const onLogoutExpectedUserId = userId; - conn.call('logout'); + await conn.callAsync('logout'); conn.disconnect(); validateStopper.stop(); @@ -491,17 +404,18 @@ Tinytest.add( } ); -Tinytest.add( +Tinytest.addAsync( 'accounts - hook callbacks obey options.defaultFieldSelector', - test => { + async test => { const ignoreFieldName = "bigArray"; - const userId = Accounts.insertUserDoc({}, { username: Random.id(), [ignoreFieldName]: [1] }); + const userId = + await Accounts.insertUserDoc({}, { username: Random.id(), [ignoreFieldName]: [1] }); const stampedToken = Accounts._generateStampedLoginToken(); - Accounts._insertLoginToken(userId, stampedToken); + await Accounts._insertLoginToken(userId, stampedToken); const options = Accounts._options; Accounts._options = {}; - Accounts.config({defaultFieldSelector: {[ignoreFieldName]: 0}}); - test.equal(Accounts._options.defaultFieldSelector, {[ignoreFieldName]: 0}, 'defaultFieldSelector'); + Accounts.config({ defaultFieldSelector: { [ignoreFieldName]: 0 } }); + test.equal(Accounts._options.defaultFieldSelector, { [ignoreFieldName]: 0 }, 'defaultFieldSelector'); const validateStopper = Accounts.validateLoginAttempt(attempt => { test.isUndefined(allowLogin != 'bogus' ? attempt.user[ignoreFieldName] : attempt.user, "validateLoginAttempt") @@ -521,23 +435,27 @@ Tinytest.add( // test a new connection let allowLogin = true; - conn.call('login', { resume: stampedToken.token }); + await conn.callAsync('login', { resume: stampedToken.token }); // Now that the user is logged in on the connection, Meteor.userId() should // return that user. - conn.call('login', { resume: stampedToken.token }); + await conn.callAsync('login', { resume: stampedToken.token }); // Trigger onLoginFailure callbacks, this will not include the user object allowLogin = 'bogus'; - test.throws(() => conn.call('login', { resume: "bogus" }), '403'); + await test.throwsAsync( + async () => + await conn.callAsync('login', { resume: "bogus" }), '403'); // test a forced login fail which WILL include the user object allowLogin = false; - test.throws(() => conn.call('login', { resume: stampedToken.token }), '403'); + await test.throwsAsync( + async () => + await conn.callAsync('login', { resume: stampedToken.token }), '403'); // Trigger onLogout callbacks const onLogoutExpectedUserId = userId; - conn.call('logout'); + await conn.callAsync('logout'); Accounts._options = options; conn.disconnect(); @@ -548,53 +466,55 @@ Tinytest.add( } ); -Tinytest.add( +Tinytest.addAsync( 'accounts - Meteor.user() obeys options.defaultFieldSelector', - test => { + async test => { const ignoreFieldName = "bigArray"; const customField = "customField"; - const userId = Accounts.insertUserDoc({}, { username: Random.id(), [ignoreFieldName]: [1], [customField]: 'test' }); + const userId = + await Accounts.insertUserDoc({}, { username: Random.id(), [ignoreFieldName]: [1], [customField]: 'test' }); const stampedToken = Accounts._generateStampedLoginToken(); - Accounts._insertLoginToken(userId, stampedToken); + await Accounts._insertLoginToken(userId, stampedToken); const options = Accounts._options; // stub Meteor.userId() so it works outside methods and returns the correct user: const origAccountsUserId = Accounts.userId; - Accounts.userId = () => userId; + Accounts.userId = + () => userId; Accounts._options = {}; // test the field is included by default - let user = Meteor.user(); + let user = await Meteor.user(); test.isNotUndefined(user[ignoreFieldName], 'included by default'); // test the field is excluded - Accounts.config({defaultFieldSelector: {[ignoreFieldName]: 0}}); - user = Meteor.user(); + Accounts.config({ defaultFieldSelector: { [ignoreFieldName]: 0 } }); + user = await Meteor.user(); test.isUndefined(user[ignoreFieldName], 'excluded'); - user = Meteor.user({}); + user = await Meteor.user({}); test.isUndefined(user[ignoreFieldName], 'excluded {}'); // test the field can still be retrieved if required - user = Meteor.user({fields: {[ignoreFieldName]: 1}}); + user = await Meteor.user({ fields: { [ignoreFieldName]: 1 } }); test.isNotUndefined(user[ignoreFieldName], 'field can be retrieved'); test.isUndefined(user.username, 'field can be retrieved username'); // test a combined negative field specifier - user = Meteor.user({fields: {username: 0}}); + user = await Meteor.user({ fields: { username: 0 } }); test.isUndefined(user[ignoreFieldName], 'combined field selector'); test.isUndefined(user.username, 'combined field selector username'); // test an explicit request for the full user object - user = Meteor.user({fields: {}}); + user = await Meteor.user({ fields: {} }); test.isNotUndefined(user[ignoreFieldName], 'full selector'); test.isNotUndefined(user.username, 'full selector username'); Accounts._options = {}; // Test that a custom field gets retrieved properly - Accounts.config({defaultFieldSelector: {[customField]: 1}}); - user = Meteor.user() + Accounts.config({ defaultFieldSelector: { [customField]: 1 } }); + user = await Meteor.user() test.isNotUndefined(user[customField]); test.isUndefined(user.username); test.isUndefined(user[ignoreFieldName]); @@ -610,14 +530,16 @@ Tinytest.addAsync( async test => { const ignoreFieldName = "bigArray"; const customField = "customField"; - const userId = Accounts.insertUserDoc({}, { username: Random.id(), [ignoreFieldName]: [1], [customField]: 'test' }); + const userId = + await Accounts.insertUserDoc({}, { username: Random.id(), [ignoreFieldName]: [1], [customField]: 'test' }); const stampedToken = Accounts._generateStampedLoginToken(); - Accounts._insertLoginToken(userId, stampedToken); + await Accounts._insertLoginToken(userId, stampedToken); const options = Accounts._options; // stub Meteor.userId() so it works outside methods and returns the correct user: const origAccountsUserId = Accounts.userId; - Accounts.userId = () => userId; + Accounts.userId = + () => userId; Accounts._options = {}; @@ -660,21 +582,25 @@ Tinytest.addAsync( Accounts.userId = origAccountsUserId; } ); -Tinytest.add( +Tinytest.addAsync( 'accounts - verify onExternalLogin hook can update oauth user profiles', - test => { + async test => { // Verify user profile data is saved properly when not using the // onExternalLogin hook. let facebookId = Random.id(); - const uid1 = Accounts.updateOrCreateUserFromExternalService( + const u1 = await Accounts.updateOrCreateUserFromExternalService( 'facebook', { id: facebookId }, { profile: { foo: 1 } }, - ).userId; + ); const ignoreFieldName = "bigArray"; - const c = Meteor.users.update(uid1, {$set: {[ignoreFieldName]: [1]}}); + + const c = + await Meteor.users.update(u1.userId, { $set: { [ignoreFieldName]: [1] } }); + let users = - Meteor.users.find({ 'services.facebook.id': facebookId }).fetch(); + await Meteor.users.find({ 'services.facebook.id': facebookId }).fetch(); + test.length(users, 1); test.equal(users[0].profile.foo, 1); test.isNotUndefined(users[0][ignoreFieldName], 'ignoreField - before limit fields'); @@ -684,19 +610,19 @@ Tinytest.add( // Also verify that the user object is filtered by _options.defaultFieldSelector const accountsOptions = Accounts._options; Accounts._options = {}; - Accounts.config({defaultFieldSelector: {[ignoreFieldName]: 0}}); + Accounts.config({ defaultFieldSelector: { [ignoreFieldName]: 0 } }); Accounts.onExternalLogin((options, user) => { options.profile.foo = 2; test.isUndefined(users[ignoreFieldName], 'ignoreField - after limit fields'); return options; }); - Accounts.updateOrCreateUserFromExternalService( + await Accounts.updateOrCreateUserFromExternalService( 'facebook', { id: facebookId }, { profile: { foo: 1 } }, ); // test.isUndefined(users[0][ignoreFieldName], 'ignoreField - fields limited'); - users = Meteor.users.find({ 'services.facebook.id': facebookId }).fetch(); + users = await Meteor.users.find({ 'services.facebook.id': facebookId }).fetch(); test.length(users, 1); test.equal(users[0].profile.foo, 2); test.isNotUndefined(users[0][ignoreFieldName], 'ignoreField - still there'); @@ -704,106 +630,116 @@ Tinytest.add( // Verify user profile data can be modified using the onExternalLogin // hook, for new users. facebookId = Random.id(); - const uid2 = Accounts.updateOrCreateUserFromExternalService( + const u2 = await Accounts.updateOrCreateUserFromExternalService( 'facebook', { id: facebookId }, { profile: { foo: 3 } }, - ).userId; - users = Meteor.users.find({ 'services.facebook.id': facebookId }).fetch(); + ); + users = await Meteor.users.find({ 'services.facebook.id': facebookId }).fetch(); test.length(users, 1); test.equal(users[0].profile.foo, 2); // Cleanup - Meteor.users.remove(uid1); - Meteor.users.remove(uid2); + await Meteor.users.remove(u1); + await Meteor.users.remove(u2.userId); Accounts._onExternalLoginHook = null; Accounts._options = accountsOptions; } ); -Tinytest.add( - 'accounts - verify beforeExternalLogin hook can stop user login', - test => { - // Verify user data is saved properly when not using the - // beforeExternalLogin hook. - let facebookId = Random.id(); - const uid1 = Accounts.updateOrCreateUserFromExternalService( - 'facebook', - { id: facebookId }, - { profile: { foo: 1 } }, - ).userId; - const ignoreFieldName = "bigArray"; - const c = Meteor.users.update(uid1, {$set: {[ignoreFieldName]: [1]}}); - let users = - Meteor.users.find({ 'services.facebook.id': facebookId }).fetch(); - test.length(users, 1); - test.equal(users[0].profile.foo, 1); - test.isNotUndefined(users[0][ignoreFieldName], 'ignoreField - before limit fields'); +Tinytest.addAsync( + 'accounts - verify beforeExternalLogin hook can stop user login', + async test => { + // Verify user data is saved properly when not using the + // beforeExternalLogin hook. + let facebookId = Random.id(); - // Verify that when beforeExternalLogin returns false - // that an error throws and user is not saved - Accounts.beforeExternalLogin((serviceName, serviceData, user) => { - // Check that we get the correct data - test.equal(serviceName, 'facebook'); - test.equal(serviceData, { id: facebookId }); - test.equal(user._id, uid1); - return false - }); + const u = + await Accounts.updateOrCreateUserFromExternalService( + 'facebook', + { id: facebookId }, + { profile: { foo: 1 } }, + ); - test.throws(() => Accounts.updateOrCreateUserFromExternalService( - 'facebook', - { id: facebookId }, - { profile: { foo: 1 } }, + const ignoreFieldName = "bigArray"; + + const c = + await Meteor.users.update(u.userId, { $set: { [ignoreFieldName]: [1] } }); + + let users = + await Meteor.users.find({ 'services.facebook.id': facebookId }).fetch(); + + test.length(users, 1); + test.equal(users[0].profile.foo, 1); + test.isNotUndefined(users[0][ignoreFieldName], 'ignoreField - before limit fields'); + + // Verify that when beforeExternalLogin returns false + // that an error throws and user is not saved + Accounts.beforeExternalLogin((serviceName, serviceData, user) => { + // Check that we get the correct data + test.equal(serviceName, 'facebook'); + test.equal(serviceData, { id: facebookId }); + test.equal(user._id, u.userId); + return false + }); + + await test.throwsAsync( + async () => + await Accounts.updateOrCreateUserFromExternalService( + 'facebook', + { id: facebookId }, + { profile: { foo: 1 } }, )); - // Cleanup - Meteor.users.remove(uid1); - Accounts._beforeExternalLoginHook = null; - } -); - -Tinytest.add( - 'accounts - verify setAdditionalFindUserOnExternalLogin hook can provide user', - test => { - // create test user, without a google service - const testEmail = "test@testdomain.com" - const uid0 = Accounts.createUser({email: testEmail}) - - // Verify that user is found from email and service merged - Accounts.setAdditionalFindUserOnExternalLogin(({serviceName, serviceData}) => { - if (serviceName === "google") { - return Accounts.findUserByEmail(serviceData.email) - } - }) - - let googleId = Random.id(); - const uid1 = Accounts.updateOrCreateUserFromExternalService( - 'google', - { id: googleId, email: testEmail }, - { profile: { foo: 1 } }, - ).userId; - - test.equal(uid0, uid1) - - // Cleanup - if (uid1 !== uid0) { - Meteor.users.remove(uid0) - } - Meteor.users.remove(uid1); - Accounts.selectCustomUserOnExternalLogin = null; + // Cleanup + await Meteor.users.remove(u.userId); + Accounts._beforeExternalLoginHook = null; } ); -if(Meteor.isServer) { - Tinytest.add( +Tinytest.addAsync( + 'accounts - verify setAdditionalFindUserOnExternalLogin hook can provide user', + async test => { + // create test user, without a google service + const testEmail = "test@testdomain.com" + // being sure that the user is not already in the database + await Meteor.users.remove({ "emails.address": testEmail }); + const uid0 = await Accounts.createUser({ email: testEmail }) + + // Verify that user is found from email and service merged + Accounts.setAdditionalFindUserOnExternalLogin(async ({ serviceName, serviceData }) => { + if (serviceName === "google") { + return await Accounts.findUserByEmail(serviceData.email) + } + }) + + let googleId = Random.id(); + const u1 = await Accounts.updateOrCreateUserFromExternalService( + 'google', + { id: googleId, email: testEmail }, + { profile: { foo: 1 } }, + ); + test.equal(uid0, u1.userId) + + // Cleanup + if (u1.userId !== uid0) { + await Meteor.users.remove(uid0) + } + await Meteor.users.remove(u1.userId); + Accounts.selectCustomUserOnExternalLogin = null; + } +); + +if (Meteor.isServer) { + Tinytest.addAsync( 'accounts - make sure that extra params to accounts urls are added', - test => { + async test => { // No extra params const verifyEmailURL = new URL(Accounts.urls.verifyEmail('test')); test.equal(verifyEmailURL.searchParams.toString(), ""); // Extra params - const extraParams = { test: 'success'}; + const extraParams = { test: 'success' }; const resetPasswordURL = new URL(Accounts.urls.resetPassword('test', extraParams)); test.equal(resetPasswordURL.searchParams.get('test'), extraParams.test); const enrollAccountURL = new URL(Accounts.urls.enrollAccount('test', extraParams)); @@ -811,3 +747,127 @@ if(Meteor.isServer) { } ); } + +Tinytest.addAsync('accounts - updateOrCreateUserFromExternalService - Facebook', async test => { + const facebookId = Random.id(); + + // create an account with facebook + const u1 = + await Accounts.updateOrCreateUserFromExternalService( + 'facebook', { id: facebookId, monkey: 42 }, { profile: { foo: 1 } }); + const users1 = + await Meteor.users.find({ "services.facebook.id": facebookId }).fetch(); + test.length(users1, 1); + test.equal(users1[0].profile.foo, 1); + test.equal(users1[0].services.facebook.monkey, 42); + + // create again with the same id, see that we get the same user. + // it should update services.facebook but not profile. + const u2 = + await Accounts.updateOrCreateUserFromExternalService( + 'facebook', { id: facebookId, llama: 50 }, + { profile: { foo: 1000, bar: 2 } }); + test.equal(u1.id, u2.id); + const users2 = + await Meteor.users.find({ "services.facebook.id": facebookId }).fetch(); + test.length(users2, 1); + test.equal(users2[0].profile.foo, 1); + test.equal(users2[0].profile.bar, undefined); + test.equal(users2[0].services.facebook.llama, 50); + // make sure we *don't* lose values not passed this call to + // updateOrCreateUserFromExternalService + test.equal(users2[0].services.facebook.monkey, 42); + + // cleanup + await Meteor.users.remove(u1.id); +}); + +Tinytest.addAsync('accounts - updateOrCreateUserFromExternalService - Meteor Developer', async test => { + const developerId = + Random.id(); + const u1 = + await Accounts.updateOrCreateUserFromExternalService( + 'meteor-developer', + { id: developerId, username: 'meteor-developer' }, + { profile: { name: 'meteor-developer' } } + ); + const users1 = + await Meteor.users.find({ 'services.meteor-developer.id': developerId }).fetch(); + test.length(users1, 1); + test.equal(users1[0].profile.name, 'meteor-developer'); + + const u2 = + await Accounts.updateOrCreateUserFromExternalService( + 'meteor-developer', + { id: developerId, username: 'meteor-developer' }, + { profile: { name: 'meteor-developer', username: 'developer' } } + ); + test.equal(u1.id, u2.id); + const users2 = + await Meteor.users.find({ 'services.meteor-developer.id': developerId }).fetch(); + test.length(users2, 1); + test.equal(users1[0].profile.name, 'meteor-developer'); + test.equal(users1[0].profile.username, undefined); + + // cleanup + await Meteor.users.remove(u1); +}); + +Tinytest.addAsync('accounts - updateOrCreateUserFromExternalService - Weibo', async test => { + const weiboId1 = + Random.id(); + const weiboId2 = + Random.id(); + + // users that have different service ids get different users + const u1 = + await Accounts.updateOrCreateUserFromExternalService( + 'weibo', { id: weiboId1 }, { profile: { foo: 1 } }); + const u2 = + await Accounts.updateOrCreateUserFromExternalService( + 'weibo', { id: weiboId2 }, { profile: { bar: 2 } }); + test.equal(await Meteor.users.find({ "services.weibo.id": { $in: [weiboId1, weiboId2] } }).count(), 2); + + const user1 = + await Meteor.users.findOne({ "services.weibo.id": weiboId1 }); + const user2 = + await Meteor.users.findOne({ "services.weibo.id": weiboId2 }); + test.equal(user1.profile.foo, 1); + test.equal(user1.emails, undefined); + test.equal(user2.profile.bar, 2); + test.equal(user2.emails, undefined); + + // cleanup + Meteor.users.remove(u1.id); + Meteor.users.remove(u2.id); +}); + +Tinytest.addAsync('accounts - updateOrCreateUserFromExternalService - Twitter', async test => { + const twitterIdOld = parseInt(Random.hexString(4), 16); + const twitterIdNew = '' + twitterIdOld; + + // create an account with twitter using the old ID format of integer + const u1 = + await Accounts.updateOrCreateUserFromExternalService( + 'twitter', { id: twitterIdOld, monkey: 42 }, { profile: { foo: 1 } }); + const users1 = + await Meteor.users.find({ "services.twitter.id": twitterIdOld }).fetch(); + test.length(users1, 1); + test.equal(users1[0].profile.foo, 1); + test.equal(users1[0].services.twitter.monkey, 42); + + // Update the account with the new ID format of string + // test that the existing user is found, and that the ID + // gets updated to a string value + const u2 = + await Accounts.updateOrCreateUserFromExternalService( + 'twitter', { id: twitterIdNew, monkey: 42 }, { profile: { foo: 1 } }); + test.equal(u1.id, u2.id); + const users2 = + await Meteor.users.find({ "services.twitter.id": twitterIdNew }).fetch(); + test.length(users2, 1); + + // cleanup + await Meteor.users.remove(u1.id); +}); + diff --git a/packages/accounts-base/accounts_tests_setup.js b/packages/accounts-base/accounts_tests_setup.js index bd79562fe0..c83ce5677b 100644 --- a/packages/accounts-base/accounts_tests_setup.js +++ b/packages/accounts-base/accounts_tests_setup.js @@ -1,25 +1,25 @@ -const getTokenFromSecret = ({ selector, secret: secretParam }) => { +const getTokenFromSecret = async ({ selector, secret: secretParam }) => { let secret = secretParam; if (!secret) { const { services: { twoFactorAuthentication } = {} } = - Meteor.users.findOne(selector) || {}; + await Meteor.users.findOne(selector) || {}; if (!twoFactorAuthentication) { throw new Meteor.Error(500, 'twoFactorAuthentication not set.'); } secret = twoFactorAuthentication.secret; } - const { token } = Accounts._generate2faToken(secret); + const { token } = await Accounts._generate2faToken(secret); return token; }; Meteor.methods({ - removeAccountsTestUser(username) { - Meteor.users.remove({ username }); + async removeAccountsTestUser(username) { + await Meteor.users.remove({ username }); }, - forceEnableUser2fa(selector, secret) { - Meteor.users.update( + async forceEnableUser2fa(selector, secret) { + await Meteor.users.update( selector, { $set: { @@ -30,7 +30,7 @@ Meteor.methods({ }, } ); - return getTokenFromSecret({ selector, secret }); + return await getTokenFromSecret({ selector, secret }); }, getTokenFromSecret, }); diff --git a/packages/accounts-base/server_main.js b/packages/accounts-base/server_main.js index db5020fed5..05ec621117 100644 --- a/packages/accounts-base/server_main.js +++ b/packages/accounts-base/server_main.js @@ -5,7 +5,8 @@ import { AccountsServer } from "./accounts_server.js"; * @summary The namespace for all server-side accounts-related methods. */ Accounts = new AccountsServer(Meteor.server); - +// TODO[FIBERS]: I need TLA +Accounts.init().then() // Users table. Don't use the normal autopublish, since we want to hide // some fields. Code to autopublish this is in accounts_server.js. // XXX Allow users to configure this collection name. @@ -15,7 +16,7 @@ Accounts = new AccountsServer(Meteor.server); * @locus Anywhere * @type {Mongo.Collection} * @importFromPackage meteor -*/ + */ Meteor.users = Accounts.users; export {