From 1eb95aeb41a96250e582d79a703f6adcb403c08b Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Mon, 15 Jan 2024 15:44:49 +0100 Subject: [PATCH] fix: Incomplete user object in `verifyEmail` function if both username and email are changed (#8889) --- spec/.eslintrc.json | 1 + spec/EmailVerificationToken.spec.js | 50 ++++++++++++++++++ spec/PagesRouter.spec.js | 7 ++- spec/Parse.Push.spec.js | 4 +- spec/ParseHooks.spec.js | 2 +- spec/ParseUser.spec.js | 1 + spec/PushController.spec.js | 10 ++-- spec/UserController.spec.js | 66 ++++++++++++++++-------- spec/ValidationAndPasswordsReset.spec.js | 5 ++ spec/helper.js | 12 ++++- src/Controllers/UserController.js | 14 ++--- 11 files changed, 129 insertions(+), 43 deletions(-) diff --git a/spec/.eslintrc.json b/spec/.eslintrc.json index ff45304cd5..cb61d2fd6c 100644 --- a/spec/.eslintrc.json +++ b/spec/.eslintrc.json @@ -28,6 +28,7 @@ "fit_exclude_node_version": true, "it_exclude_dbs": true, "describe_only_db": true, + "fdescribe_only_db": true, "describe_only": true, "on_db": true, "defaultConfiguration": true, diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 82ca60d495..39ad6e5edd 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -127,6 +127,7 @@ describe('Email Verification Token Expiration: ', () => { user.set('email', 'user@parse.com'); return user.signUp(); }) + .then(() => jasmine.timeout()) .then(() => { request({ url: sendEmailOptions.link, @@ -168,6 +169,7 @@ describe('Email Verification Token Expiration: ', () => { user.set('email', 'user@parse.com'); return user.signUp(); }) + .then(() => jasmine.timeout()) .then(() => { request({ url: sendEmailOptions.link, @@ -215,6 +217,7 @@ describe('Email Verification Token Expiration: ', () => { user.set('email', 'user@parse.com'); return user.signUp(); }) + .then(() => jasmine.timeout()) .then(() => { request({ url: sendEmailOptions.link, @@ -388,6 +391,7 @@ describe('Email Verification Token Expiration: ', () => { user2.setPassword('expiringToken'); user2.set('email', 'user2@example.com'); await user2.signUp(); + await jasmine.timeout(); expect(user2.getSessionToken()).toBeUndefined(); expect(sendEmailOptions).toBeDefined(); expect(verifySpy).toHaveBeenCalledTimes(5); @@ -422,10 +426,47 @@ describe('Email Verification Token Expiration: ', () => { newUser.set('email', 'user@example.com'); await newUser.signUp(); await Parse.User.requestEmailVerification('user@example.com'); + await jasmine.timeout(); expect(sendSpy).toHaveBeenCalledTimes(2); expect(emailSpy).toHaveBeenCalledTimes(0); }); + it('provides full user object in email verification function on email and username change', async () => { + const emailAdapter = { + sendVerificationEmail: () => {}, + sendPasswordResetEmail: () => Promise.resolve(), + sendMail: () => {}, + }; + const sendVerificationEmail = { + method(req) { + expect(req.user).toBeDefined(); + expect(req.user.id).toBeDefined(); + expect(req.user.get('createdAt')).toBeDefined(); + expect(req.user.get('updatedAt')).toBeDefined(); + expect(req.master).toBeDefined(); + return false; + }, + }; + await reconfigureServer({ + appName: 'emailVerifyToken', + verifyUserEmails: true, + emailAdapter: emailAdapter, + emailVerifyTokenValidityDuration: 5, + publicServerURL: 'http://localhost:8378/1', + sendUserEmailVerification: sendVerificationEmail.method, + }); + const user = new Parse.User(); + user.setPassword('password'); + user.setUsername('new@example.com'); + user.setEmail('user@example.com'); + await user.save(null, { useMasterKey: true }); + + // Update email and username + user.setUsername('new@example.com'); + user.setEmail('new@example.com'); + await user.save(null, { useMasterKey: true }); + }); + it('beforeSave options do not change existing behaviour', async () => { let sendEmailOptions; const emailAdapter = { @@ -448,6 +489,7 @@ describe('Email Verification Token Expiration: ', () => { newUser.setPassword('expiringToken'); newUser.set('email', 'user@parse.com'); await newUser.signUp(); + await jasmine.timeout(); const response = await request({ url: sendEmailOptions.link, followRedirects: false, @@ -490,6 +532,7 @@ describe('Email Verification Token Expiration: ', () => { user.set('email', 'user@parse.com'); return user.signUp(); }) + .then(() => jasmine.timeout()) .then(() => { request({ url: sendEmailOptions.link, @@ -549,6 +592,7 @@ describe('Email Verification Token Expiration: ', () => { user.set('email', 'user@parse.com'); return user.signUp(); }) + .then(() => jasmine.timeout()) .then(() => { return request({ url: sendEmailOptions.link, @@ -766,6 +810,9 @@ describe('Email Verification Token Expiration: ', () => { }) .then(response => { expect(response.status).toBe(200); + }) + .then(() => jasmine.timeout()) + .then(() => { expect(sendVerificationEmailCallCount).toBe(2); expect(sendEmailOptions).toBeDefined(); @@ -917,6 +964,7 @@ describe('Email Verification Token Expiration: ', () => { 'Content-Type': 'application/json', }, }); + await jasmine.timeout(); expect(response.status).toBe(200); expect(sendVerificationEmailCallCount).toBe(2); expect(sendEmailOptions).toBeDefined(); @@ -959,6 +1007,7 @@ describe('Email Verification Token Expiration: ', () => { user.set('email', 'user@parse.com'); return user.signUp(); }) + .then(() => jasmine.timeout()) .then(() => { return request({ url: sendEmailOptions.link, @@ -1197,6 +1246,7 @@ describe('Email Verification Token Expiration: ', () => { user.set('email', 'user@parse.com'); return user.signUp(); }) + .then(() => jasmine.timeout()) .then(() => { request({ url: sendEmailOptions.link, diff --git a/spec/PagesRouter.spec.js b/spec/PagesRouter.spec.js index e50144f1fe..5f86922b08 100644 --- a/spec/PagesRouter.spec.js +++ b/spec/PagesRouter.spec.js @@ -749,6 +749,7 @@ describe('Pages Router', () => { user.setPassword('examplePassword'); user.set('email', 'mail@example.com'); await user.signUp(); + await jasmine.timeout(); const link = sendVerificationEmail.calls.all()[0].args[0].link; const linkWithLocale = new URL(link); @@ -777,6 +778,7 @@ describe('Pages Router', () => { user.setPassword('examplePassword'); user.set('email', 'mail@example.com'); await user.signUp(); + await jasmine.timeout(); const link = sendVerificationEmail.calls.all()[0].args[0].link; const linkWithLocale = new URL(link); @@ -830,6 +832,7 @@ describe('Pages Router', () => { user.setPassword('examplePassword'); user.set('email', 'mail@example.com'); await user.signUp(); + await jasmine.timeout(); const link = sendVerificationEmail.calls.all()[0].args[0].link; const linkWithLocale = new URL(link); @@ -846,6 +849,8 @@ describe('Pages Router', () => { const locale = linkResponse.headers['x-parse-page-param-locale']; const username = linkResponse.headers['x-parse-page-param-username']; const publicServerUrl = linkResponse.headers['x-parse-page-param-publicserverurl']; + await jasmine.timeout(); + const invalidVerificationPagePath = pageResponse.calls.all()[0].args[0]; expect(appId).toBeDefined(); expect(locale).toBe(exampleLocale); @@ -1190,6 +1195,7 @@ describe('Pages Router', () => { user.setPassword('examplePassword'); user.set('email', 'mail@example.com'); await user.signUp(); + await jasmine.timeout(); const link = sendVerificationEmail.calls.all()[0].args[0].link; const linkResponse = await request({ @@ -1197,7 +1203,6 @@ describe('Pages Router', () => { followRedirects: false, }); expect(linkResponse.status).toBe(200); - const pagePath = pageResponse.calls.all()[0].args[0]; expect(pagePath).toMatch(new RegExp(`\/${pages.emailVerificationSuccess.defaultFile}`)); }); diff --git a/spec/Parse.Push.spec.js b/spec/Parse.Push.spec.js index 1732e426e3..143922852d 100644 --- a/spec/Parse.Push.spec.js +++ b/spec/Parse.Push.spec.js @@ -2,14 +2,12 @@ const request = require('../lib/request'); -const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); - const pushCompleted = async pushId => { const query = new Parse.Query('_PushStatus'); query.equalTo('objectId', pushId); let result = await query.first({ useMasterKey: true }); while (!(result && result.get('status') === 'succeeded')) { - await sleep(100); + await jasmine.timeout(); result = await query.first({ useMasterKey: true }); } }; diff --git a/spec/ParseHooks.spec.js b/spec/ParseHooks.spec.js index f4bcc2e440..16a2e17be3 100644 --- a/spec/ParseHooks.spec.js +++ b/spec/ParseHooks.spec.js @@ -190,7 +190,7 @@ describe('Hooks', () => { it('should fail trying to create two times the same function', done => { Parse.Hooks.createFunction('my_new_function', 'http://url.com') - .then(() => new Promise(resolve => setTimeout(resolve, 100))) + .then(() => jasmine.timeout()) .then( () => { return Parse.Hooks.createFunction('my_new_function', 'http://url.com'); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 43351f630c..5a4a81d0a7 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -3067,6 +3067,7 @@ describe('Parse.User testing', () => { }, }); }) + .then(() => jasmine.timeout()) .then(() => { expect(emailCalled).toBe(true); expect(emailOptions).not.toBeUndefined(); diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index 49613214f5..572fc9bcf9 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -26,14 +26,12 @@ const successfulIOS = function (body, installations) { return Promise.all(promises); }; -const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); - const pushCompleted = async pushId => { const query = new Parse.Query('_PushStatus'); query.equalTo('objectId', pushId); let result = await query.first({ useMasterKey: true }); while (!(result && result.get('status') === 'succeeded')) { - await sleep(100); + await jasmine.timeout(); result = await query.first({ useMasterKey: true }); } }; @@ -562,7 +560,7 @@ describe('PushController', () => { }); const pushStatusId = await sendPush(payload, {}, config, auth); // it is enqueued so it can take time - await sleep(1000); + await jasmine.timeout(1000); Parse.serverURL = 'http://localhost:8378/1'; // GOOD url const result = await Parse.Push.getPushStatus(pushStatusId); expect(result).toBeDefined(); @@ -801,7 +799,7 @@ describe('PushController', () => { }); await Parse.Object.saveAll(installations); await pushController.sendPush(payload, {}, config, auth); - await sleep(1000); + await jasmine.timeout(1000); const query = new Parse.Query('_PushStatus'); const results = await query.find({ useMasterKey: true }); expect(results.length).toBe(1); @@ -856,7 +854,7 @@ describe('PushController', () => { const config = Config.get(Parse.applicationId); await Parse.Object.saveAll(installations); await pushController.sendPush(payload, {}, config, auth); - await sleep(1000); + await jasmine.timeout(1000); const query = new Parse.Query('_PushStatus'); const results = await query.find({ useMasterKey: true }); expect(results.length).toBe(1); diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index 7b98367702..03c979abe6 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -1,16 +1,12 @@ const emailAdapter = require('./support/MockEmailAdapter'); +const Config = require('../lib/Config'); +const Auth = require('../lib/Auth'); describe('UserController', () => { - const user = { - _email_verify_token: 'testToken', - username: 'testUser', - email: 'test@example.com', - }; - describe('sendVerificationEmail', () => { describe('parseFrameURL not provided', () => { - it('uses publicServerURL', async done => { - const server = await reconfigureServer({ + it('uses publicServerURL', async () => { + await reconfigureServer({ publicServerURL: 'http://www.example.com', customPages: { parseFrameURL: undefined, @@ -19,20 +15,33 @@ describe('UserController', () => { emailAdapter, appName: 'test', }); + + let emailOptions; emailAdapter.sendVerificationEmail = options => { - expect(options.link).toEqual( - 'http://www.example.com/apps/test/verify_email?token=testToken&username=testUser' - ); - emailAdapter.sendVerificationEmail = () => Promise.resolve(); - done(); + emailOptions = options; + return Promise.resolve(); }; - server.config.userController.sendVerificationEmail(user); + + const username = 'verificationUser'; + const user = new Parse.User(); + user.setUsername(username); + user.setPassword('pass'); + user.setEmail('verification@example.com'); + await user.signUp(); + + const config = Config.get('test'); + const rawUser = await config.database.find('_User', { username }, {}, Auth.maintenance(config)); + const rawUsername = rawUser[0].username; + const rawToken = rawUser[0]._email_verify_token; + expect(rawToken).toBeDefined(); + expect(rawUsername).toBe(username); + expect(emailOptions.link).toEqual(`http://www.example.com/apps/test/verify_email?token=${rawToken}&username=${username}`); }); }); describe('parseFrameURL provided', () => { - it('uses parseFrameURL and includes the destination in the link parameter', async done => { - const server = await reconfigureServer({ + it('uses parseFrameURL and includes the destination in the link parameter', async () => { + await reconfigureServer({ publicServerURL: 'http://www.example.com', customPages: { parseFrameURL: 'http://someother.example.com/handle-parse-iframe', @@ -41,14 +50,27 @@ describe('UserController', () => { emailAdapter, appName: 'test', }); + + let emailOptions; emailAdapter.sendVerificationEmail = options => { - expect(options.link).toEqual( - 'http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser' - ); - emailAdapter.sendVerificationEmail = () => Promise.resolve(); - done(); + emailOptions = options; + return Promise.resolve(); }; - server.config.userController.sendVerificationEmail(user); + + const username = 'verificationUser'; + const user = new Parse.User(); + user.setUsername(username); + user.setPassword('pass'); + user.setEmail('verification@example.com'); + await user.signUp(); + + const config = Config.get('test'); + const rawUser = await config.database.find('_User', { username }, {}, Auth.maintenance(config)); + const rawUsername = rawUser[0].username; + const rawToken = rawUser[0]._email_verify_token; + expect(rawToken).toBeDefined(); + expect(rawUsername).toBe(username); + expect(emailOptions.link).toEqual(`http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=${rawToken}&username=${username}`); }); }); }); diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index 0845abeda1..fb163ad567 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -51,6 +51,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { user.setUsername('zxcv'); user.setEmail('testIfEnabled@parse.com'); await user.signUp(); + await jasmine.timeout(); expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled(); user.fetch().then(() => { expect(user.get('emailVerified')).toEqual(false); @@ -184,6 +185,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { user.setUsername('zxcv'); user.set('email', 'testSendSimpleAdapter@parse.com'); await user.signUp(); + await jasmine.timeout(); expect(calls).toBe(1); user .fetch() @@ -324,6 +326,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { user.setUsername('user'); user.set('email', 'user@example.com'); await user.signUp(); + await jasmine.timeout(); expect(sendEmailOptions).not.toBeUndefined(); const response = await request({ url: sendEmailOptions.link, @@ -635,6 +638,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { user.setUsername('zxcv'); user.set('email', 'user@parse.com'); await user.signUp(); + await jasmine.timeout(); expect(emailSent).toBe(true); done(); }); @@ -662,6 +666,7 @@ describe('Custom Pages, Email Verification, Password Reset', () => { user.set('email', 'user@parse.com'); return user.signUp(); }) + .then(() => jasmine.timeout()) .then(() => { expect(sendEmailOptions).not.toBeUndefined(); request({ diff --git a/spec/helper.js b/spec/helper.js index e4d93282fc..f17f105e85 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -570,6 +570,16 @@ global.describe_only_db = db => { } }; +global.fdescribe_only_db = db => { + if (process.env.PARSE_SERVER_TEST_DB == db) { + return fdescribe; + } else if (!process.env.PARSE_SERVER_TEST_DB && db == 'mongo') { + return fdescribe; + } else { + return xdescribe; + } +}; + global.describe_only = validator => { if (validator()) { return describe; @@ -595,4 +605,4 @@ jasmine.restoreLibrary = function (library, name) { require(library)[name] = libraryCache[library][name]; }; -jasmine.timeout = t => new Promise(resolve => setTimeout(resolve, t)); +jasmine.timeout = (t = 100) => new Promise(resolve => setTimeout(resolve, t)); diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index ba72b476a1..0e8e18ae7d 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -129,9 +129,6 @@ export class UserController extends AdaptableController { } async getUserIfNeeded(user) { - if (user.username && user.email) { - return Promise.resolve(user); - } var where = {}; if (user.username) { where.username = user.username; @@ -148,12 +145,11 @@ export class UserController extends AdaptableController { className: '_User', restWhere: where, }); - return query.execute().then(function (result) { - if (result.results.length != 1) { - throw undefined; - } - return result.results[0]; - }); + const result = await query.execute(); + if (result.results.length != 1) { + throw undefined; + } + return result.results[0]; } async sendVerificationEmail(user, req) {