From c354da33f623616ff7d543ce509de45017899d2c Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Thu, 6 Nov 2025 21:02:55 +0100 Subject: [PATCH 1/4] feat: add beforePasswordResetRequest hook --- spec/CloudCode.spec.js | 282 +++++++++++++++++++++++++++++----- src/Routers/UsersRouter.js | 44 +++++- src/cloud-code/Parse.Cloud.js | 43 ++++++ src/triggers.js | 6 +- 4 files changed, 328 insertions(+), 47 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 308c7731b8..7ba2d16a65 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3307,19 +3307,19 @@ describe('afterFind hooks', () => { }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); expect(() => { Parse.Cloud.beforeLogin('SomeClass', () => { }); - }).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); + }).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'); expect(() => { Parse.Cloud.afterLogin(() => { }); - }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); + }).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'); expect(() => { Parse.Cloud.afterLogin('_User', () => { }); - }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); + }).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'); expect(() => { Parse.Cloud.afterLogin(Parse.User, () => { }); - }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); + }).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'); expect(() => { Parse.Cloud.afterLogin('SomeClass', () => { }); - }).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); + }).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'); expect(() => { Parse.Cloud.afterLogout(() => { }); }).not.toThrow(); @@ -3777,60 +3777,258 @@ describe('beforeLogin hook', () => { await Parse.User.logIn('tupac', 'shakur'); done(); }); +}); + +describe('beforePasswordResetRequest hook', () => { + it('should run beforePasswordResetRequest with valid user', async done => { + let hit = 0; + let sendPasswordResetEmailCalled = false; + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => { + sendPasswordResetEmailCalled = true; + }, + sendMail: () => {}, + }; - it('afterFind should not be triggered when saving an object', async () => { - let beforeSaves = 0; - Parse.Cloud.beforeSave('SavingTest', () => { - beforeSaves++; + await reconfigureServer({ + appName: 'test', + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', }); - let afterSaves = 0; - Parse.Cloud.afterSave('SavingTest', () => { - afterSaves++; + Parse.Cloud.beforePasswordResetRequest(req => { + hit++; + expect(req.object).toBeDefined(); + expect(req.object.get('email')).toEqual('test@parse.com'); + expect(req.object.get('username')).toEqual('testuser'); }); - let beforeFinds = 0; - Parse.Cloud.beforeFind('SavingTest', () => { - beforeFinds++; + const user = new Parse.User(); + user.setUsername('testuser'); + user.setPassword('password'); + user.set('email', 'test@parse.com'); + await user.signUp(); + + await Parse.User.requestPasswordReset('test@parse.com'); + expect(hit).toBe(1); + expect(sendPasswordResetEmailCalled).toBe(true); + done(); + }); + + it('should be able to block password reset request if an error is thrown', async done => { + let hit = 0; + let sendPasswordResetEmailCalled = false; + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => { + sendPasswordResetEmailCalled = true; + }, + sendMail: () => {}, + }; + + await reconfigureServer({ + appName: 'test', + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', }); - let afterFinds = 0; - Parse.Cloud.afterFind('SavingTest', () => { - afterFinds++; + Parse.Cloud.beforePasswordResetRequest(req => { + hit++; + if (req.object.get('isBanned')) { + throw new Error('banned account'); + } }); - const obj = new Parse.Object('SavingTest'); - obj.set('someField', 'some value 1'); - await obj.save(); + const user = new Parse.User(); + user.setUsername('banneduser'); + user.setPassword('password'); + user.set('email', 'banned@parse.com'); + await user.signUp(); + await user.save({ isBanned: true }); - expect(beforeSaves).toEqual(1); - expect(afterSaves).toEqual(1); - expect(beforeFinds).toEqual(0); - expect(afterFinds).toEqual(0); + try { + await Parse.User.requestPasswordReset('banned@parse.com'); + throw new Error('should not have sent password reset email.'); + } catch (e) { + expect(e.message).toBe('banned account'); + } + expect(hit).toBe(1); + expect(sendPasswordResetEmailCalled).toBe(false); + done(); + }); - obj.set('someField', 'some value 2'); - await obj.save(); + it('should be able to block password reset request if an error is thrown even if the user has an attached file', async done => { + let hit = 0; + let sendPasswordResetEmailCalled = false; + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => { + sendPasswordResetEmailCalled = true; + }, + sendMail: () => {}, + }; - expect(beforeSaves).toEqual(2); - expect(afterSaves).toEqual(2); - expect(beforeFinds).toEqual(0); - expect(afterFinds).toEqual(0); + await reconfigureServer({ + appName: 'test', + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); - await obj.fetch(); + Parse.Cloud.beforePasswordResetRequest(req => { + hit++; + if (req.object.get('isBanned')) { + throw new Error('banned account'); + } + }); - expect(beforeSaves).toEqual(2); - expect(afterSaves).toEqual(2); - expect(beforeFinds).toEqual(1); - expect(afterFinds).toEqual(1); + const user = new Parse.User(); + user.setUsername('banneduser2'); + user.setPassword('password'); + user.set('email', 'banned2@parse.com'); + await user.signUp(); + const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE='; + const file = new Parse.File('myfile.txt', { base64 }); + await file.save(); + await user.save({ isBanned: true, file }); - obj.set('someField', 'some value 3'); - await obj.save(); + try { + await Parse.User.requestPasswordReset('banned2@parse.com'); + throw new Error('should not have sent password reset email.'); + } catch (e) { + expect(e.message).toBe('banned account'); + } + expect(hit).toBe(1); + expect(sendPasswordResetEmailCalled).toBe(false); + done(); + }); + + it('should not run beforePasswordResetRequest if email does not exist', async done => { + let hit = 0; + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => {}, + sendMail: () => {}, + }; + + await reconfigureServer({ + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); + + Parse.Cloud.beforePasswordResetRequest(req => { + hit++; + }); + + try { + await Parse.User.requestPasswordReset('nonexistent@parse.com'); + } catch (e) { + // May or may not throw depending on passwordPolicy.resetPasswordSuccessOnInvalidEmail + } + expect(hit).toBe(0); + done(); + }); + + it('should have expected data in request in beforePasswordResetRequest', async done => { + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => {}, + sendMail: () => {}, + }; + + await reconfigureServer({ + appName: 'test', + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); + + Parse.Cloud.beforePasswordResetRequest(req => { + expect(req.object).toBeDefined(); + expect(req.object.get('email')).toBeDefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + expect(req.config).toBeDefined(); + }); + + const user = new Parse.User(); + user.setUsername('testuser2'); + user.setPassword('password'); + user.set('email', 'test2@parse.com'); + await user.signUp(); + await Parse.User.requestPasswordReset('test2@parse.com'); + done(); + }); + + it('should validate that only _User class is allowed for beforePasswordResetRequest', () => { + expect(() => { + Parse.Cloud.beforePasswordResetRequest('SomeClass', () => { }); + }).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'); + expect(() => { + Parse.Cloud.beforePasswordResetRequest(() => { }); + }).not.toThrow(); + expect(() => { + Parse.Cloud.beforePasswordResetRequest('_User', () => { }); + }).not.toThrow(); + expect(() => { + Parse.Cloud.beforePasswordResetRequest(Parse.User, () => { }); + }).not.toThrow(); + }); +}); + +it('afterFind should not be triggered when saving an object', async () => { + let beforeSaves = 0; + Parse.Cloud.beforeSave('SavingTest', () => { + beforeSaves++; + }); + + let afterSaves = 0; + Parse.Cloud.afterSave('SavingTest', () => { + afterSaves++; + }); + + let beforeFinds = 0; + Parse.Cloud.beforeFind('SavingTest', () => { + beforeFinds++; + }); - expect(beforeSaves).toEqual(3); - expect(afterSaves).toEqual(3); - expect(beforeFinds).toEqual(1); - expect(afterFinds).toEqual(1); + let afterFinds = 0; + Parse.Cloud.afterFind('SavingTest', () => { + afterFinds++; }); + + const obj = new Parse.Object('SavingTest'); + obj.set('someField', 'some value 1'); + await obj.save(); + + expect(beforeSaves).toEqual(1); + expect(afterSaves).toEqual(1); + expect(beforeFinds).toEqual(0); + expect(afterFinds).toEqual(0); + + obj.set('someField', 'some value 2'); + await obj.save(); + + expect(beforeSaves).toEqual(2); + expect(afterSaves).toEqual(2); + expect(beforeFinds).toEqual(0); + expect(afterFinds).toEqual(0); + + await obj.fetch(); + + expect(beforeSaves).toEqual(2); + expect(afterSaves).toEqual(2); + expect(beforeFinds).toEqual(1); + expect(afterFinds).toEqual(1); + + obj.set('someField', 'some value 3'); + await obj.save(); + + expect(beforeSaves).toEqual(3); + expect(afterSaves).toEqual(3); + expect(beforeFinds).toEqual(1); + expect(afterFinds).toEqual(1); }); describe('afterLogin hook', () => { diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 7668562965..406394ffa8 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -12,6 +12,7 @@ import { Types as TriggerTypes, getRequestObject, resolveError, + inflate, } from '../triggers'; import { promiseEnsureIdempotency } from '../middlewares'; import RestWrite from '../RestWrite'; @@ -444,21 +445,58 @@ export class UsersRouter extends ClassesRouter { if (!email && !token) { throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email'); } + + let userResults = null; + let userData = null; + + // We can find the user using token if (token) { - const results = await req.config.database.find('_User', { + userResults = await req.config.database.find('_User', { _perishable_token: token, _perishable_token_expires_at: { $lt: Parse._encode(new Date()) }, }); - if (results && results[0] && results[0].email) { - email = results[0].email; + if (userResults && userResults.length > 0) { + userData = userResults[0]; + if (userData.email) { + email = userData.email; + } + } + // Or using email if no token provided + } else if (typeof email === 'string') { + userResults = await req.config.database.find( + '_User', + { $or: [{ email }, { username: email, email: { $exists: false } }] }, + { limit: 1 }, + Auth.maintenance(req.config) + ); + if (userResults && userResults.length > 0) { + userData = userResults[0]; } } + if (typeof email !== 'string') { throw new Parse.Error( Parse.Error.INVALID_EMAIL_ADDRESS, 'you must provide a valid email string' ); } + + if (userData) { + // Useful to get User attached files in the trigger (photo picture for example) + await req.config.filesController.expandFilesInObject(req.config, userData); + + const user = inflate('_User', userData); + + await maybeRunTrigger( + TriggerTypes.beforePasswordResetRequest, + req.auth, + user, + null, + req.config, + req.info.context + ); + } + const userController = req.config.userController; try { await userController.sendPasswordResetEmail(email); diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index fa982de8f3..45262ec277 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -349,6 +349,49 @@ ParseCloud.afterLogout = function (handler) { triggers.addTrigger(triggers.Types.afterLogout, className, handler, Parse.applicationId); }; +/** + * + * Registers the before password reset request function. + * + * **Available in Cloud Code only.** + * + * This function provides control in validating a password reset request + * before the reset email is sent. It is triggered after the user is found + * by email, but before the reset token is generated and the email is sent. + * + * ``` + * Parse.Cloud.beforePasswordResetRequest((request) => { + * // Validate email or user properties + * if (!request.object.get('emailVerified')) { + * throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'Email not verified'); + * } + * }) + * + * ``` + * + * @method beforePasswordResetRequest + * @name Parse.Cloud.beforePasswordResetRequest + * @param {Function} func The function to run before a password reset request. This function can be async and should take one parameter a {@link Parse.Cloud.TriggerRequest}; + */ +ParseCloud.beforePasswordResetRequest = function (handler, validationHandler) { + let className = '_User'; + if (typeof handler === 'string' || isParseObjectConstructor(handler)) { + // validation will occur downstream, this is to maintain internal + // code consistency with the other hook types. + className = triggers.getClassName(handler); + handler = arguments[1]; + validationHandler = arguments.length >= 2 ? arguments[2] : null; + } + triggers.addTrigger(triggers.Types.beforePasswordResetRequest, className, handler, Parse.applicationId); + if (validationHandler && validationHandler.rateLimit) { + addRateLimit( + { requestPath: `/requestPasswordReset`, requestMethods: 'POST', ...validationHandler.rateLimit }, + Parse.applicationId, + true + ); + } +}; + /** * Registers an after save function. * diff --git a/src/triggers.js b/src/triggers.js index 26b107f062..e6abf20bb4 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -6,6 +6,7 @@ export const Types = { beforeLogin: 'beforeLogin', afterLogin: 'afterLogin', afterLogout: 'afterLogout', + beforePasswordResetRequest: 'beforePasswordResetRequest', beforeSave: 'beforeSave', afterSave: 'afterSave', beforeDelete: 'beforeDelete', @@ -58,10 +59,10 @@ function validateClassNameForTriggers(className, type) { // TODO: Allow proper documented way of using nested increment ops throw 'Only afterSave is allowed on _PushStatus'; } - if ((type === Types.beforeLogin || type === Types.afterLogin) && className !== '_User') { + if ((type === Types.beforeLogin || type === Types.afterLogin || type === Types.beforePasswordResetRequest) && className !== '_User') { // TODO: check if upstream code will handle `Error` instance rather // than this anti-pattern of throwing strings - throw 'Only the _User class is allowed for the beforeLogin and afterLogin triggers'; + throw 'Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'; } if (type === Types.afterLogout && className !== '_Session') { // TODO: check if upstream code will handle `Error` instance rather @@ -287,6 +288,7 @@ export function getRequestObject( triggerType === Types.afterDelete || triggerType === Types.beforeLogin || triggerType === Types.afterLogin || + triggerType === Types.beforePasswordResetRequest || triggerType === Types.afterFind ) { // Set a copy of the context on the request object. From e30572d4edd43ba83ab9bdbb74857d29310e5ea3 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Fri, 7 Nov 2025 08:07:40 +0100 Subject: [PATCH 2/4] fix: feedbacks --- src/Routers/UsersRouter.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 406394ffa8..7efb196ca6 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -482,6 +482,7 @@ export class UsersRouter extends ClassesRouter { } if (userData) { + this._sanitizeAuthData(userData); // Useful to get User attached files in the trigger (photo picture for example) await req.config.filesController.expandFilesInObject(req.config, userData); From 8076a65ea1aaf069eb43313d0ec4d803c9df2ff2 Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Sat, 8 Nov 2025 14:43:06 +0100 Subject: [PATCH 3/4] fix: test placement missing --- spec/CloudCode.spec.js | 105 +++++++++++++++++++++-------------------- 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 7ba2d16a65..7c0fa957d9 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3620,6 +3620,59 @@ describe('afterFind hooks', () => { expect(calledBefore).toBe(true); expect(calledAfter).toBe(true); }); + it('afterFind should not be triggered when saving an object', async () => { + let beforeSaves = 0; + Parse.Cloud.beforeSave('SavingTest', () => { + beforeSaves++; + }); + + let afterSaves = 0; + Parse.Cloud.afterSave('SavingTest', () => { + afterSaves++; + }); + + let beforeFinds = 0; + Parse.Cloud.beforeFind('SavingTest', () => { + beforeFinds++; + }); + + let afterFinds = 0; + Parse.Cloud.afterFind('SavingTest', () => { + afterFinds++; + }); + + const obj = new Parse.Object('SavingTest'); + obj.set('someField', 'some value 1'); + await obj.save(); + + expect(beforeSaves).toEqual(1); + expect(afterSaves).toEqual(1); + expect(beforeFinds).toEqual(0); + expect(afterFinds).toEqual(0); + + obj.set('someField', 'some value 2'); + await obj.save(); + + expect(beforeSaves).toEqual(2); + expect(afterSaves).toEqual(2); + expect(beforeFinds).toEqual(0); + expect(afterFinds).toEqual(0); + + await obj.fetch(); + + expect(beforeSaves).toEqual(2); + expect(afterSaves).toEqual(2); + expect(beforeFinds).toEqual(1); + expect(afterFinds).toEqual(1); + + obj.set('someField', 'some value 3'); + await obj.save(); + + expect(beforeSaves).toEqual(3); + expect(afterSaves).toEqual(3); + expect(beforeFinds).toEqual(1); + expect(afterFinds).toEqual(1); + }); }); describe('beforeLogin hook', () => { @@ -3977,59 +4030,7 @@ describe('beforePasswordResetRequest hook', () => { }); }); -it('afterFind should not be triggered when saving an object', async () => { - let beforeSaves = 0; - Parse.Cloud.beforeSave('SavingTest', () => { - beforeSaves++; - }); - - let afterSaves = 0; - Parse.Cloud.afterSave('SavingTest', () => { - afterSaves++; - }); - - let beforeFinds = 0; - Parse.Cloud.beforeFind('SavingTest', () => { - beforeFinds++; - }); - - let afterFinds = 0; - Parse.Cloud.afterFind('SavingTest', () => { - afterFinds++; - }); - const obj = new Parse.Object('SavingTest'); - obj.set('someField', 'some value 1'); - await obj.save(); - - expect(beforeSaves).toEqual(1); - expect(afterSaves).toEqual(1); - expect(beforeFinds).toEqual(0); - expect(afterFinds).toEqual(0); - - obj.set('someField', 'some value 2'); - await obj.save(); - - expect(beforeSaves).toEqual(2); - expect(afterSaves).toEqual(2); - expect(beforeFinds).toEqual(0); - expect(afterFinds).toEqual(0); - - await obj.fetch(); - - expect(beforeSaves).toEqual(2); - expect(afterSaves).toEqual(2); - expect(beforeFinds).toEqual(1); - expect(afterFinds).toEqual(1); - - obj.set('someField', 'some value 3'); - await obj.save(); - - expect(beforeSaves).toEqual(3); - expect(afterSaves).toEqual(3); - expect(beforeFinds).toEqual(1); - expect(afterFinds).toEqual(1); -}); describe('afterLogin hook', () => { it('should run afterLogin after successful login', async done => { From 659ab5e9eba6980aec04e9f1e53ff8dfc625a16a Mon Sep 17 00:00:00 2001 From: Lucas Coratger <73360179+coratgerl@users.noreply.github.com> Date: Sat, 8 Nov 2025 14:48:42 +0100 Subject: [PATCH 4/4] fix: test format --- spec/CloudCode.spec.js | 471 ++++++++++++++++++++--------------------- 1 file changed, 235 insertions(+), 236 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 7c0fa957d9..d7a74dd3e1 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -3620,59 +3620,6 @@ describe('afterFind hooks', () => { expect(calledBefore).toBe(true); expect(calledAfter).toBe(true); }); - it('afterFind should not be triggered when saving an object', async () => { - let beforeSaves = 0; - Parse.Cloud.beforeSave('SavingTest', () => { - beforeSaves++; - }); - - let afterSaves = 0; - Parse.Cloud.afterSave('SavingTest', () => { - afterSaves++; - }); - - let beforeFinds = 0; - Parse.Cloud.beforeFind('SavingTest', () => { - beforeFinds++; - }); - - let afterFinds = 0; - Parse.Cloud.afterFind('SavingTest', () => { - afterFinds++; - }); - - const obj = new Parse.Object('SavingTest'); - obj.set('someField', 'some value 1'); - await obj.save(); - - expect(beforeSaves).toEqual(1); - expect(afterSaves).toEqual(1); - expect(beforeFinds).toEqual(0); - expect(afterFinds).toEqual(0); - - obj.set('someField', 'some value 2'); - await obj.save(); - - expect(beforeSaves).toEqual(2); - expect(afterSaves).toEqual(2); - expect(beforeFinds).toEqual(0); - expect(afterFinds).toEqual(0); - - await obj.fetch(); - - expect(beforeSaves).toEqual(2); - expect(afterSaves).toEqual(2); - expect(beforeFinds).toEqual(1); - expect(afterFinds).toEqual(1); - - obj.set('someField', 'some value 3'); - await obj.save(); - - expect(beforeSaves).toEqual(3); - expect(afterSaves).toEqual(3); - expect(beforeFinds).toEqual(1); - expect(afterFinds).toEqual(1); - }); }); describe('beforeLogin hook', () => { @@ -3830,208 +3777,62 @@ describe('beforeLogin hook', () => { await Parse.User.logIn('tupac', 'shakur'); done(); }); -}); - -describe('beforePasswordResetRequest hook', () => { - it('should run beforePasswordResetRequest with valid user', async done => { - let hit = 0; - let sendPasswordResetEmailCalled = false; - const emailAdapter = { - sendVerificationEmail: () => Promise.resolve(), - sendPasswordResetEmail: () => { - sendPasswordResetEmailCalled = true; - }, - sendMail: () => {}, - }; - - await reconfigureServer({ - appName: 'test', - emailAdapter: emailAdapter, - publicServerURL: 'http://localhost:8378/1', - }); - - Parse.Cloud.beforePasswordResetRequest(req => { - hit++; - expect(req.object).toBeDefined(); - expect(req.object.get('email')).toEqual('test@parse.com'); - expect(req.object.get('username')).toEqual('testuser'); - }); - - const user = new Parse.User(); - user.setUsername('testuser'); - user.setPassword('password'); - user.set('email', 'test@parse.com'); - await user.signUp(); - - await Parse.User.requestPasswordReset('test@parse.com'); - expect(hit).toBe(1); - expect(sendPasswordResetEmailCalled).toBe(true); - done(); - }); - it('should be able to block password reset request if an error is thrown', async done => { - let hit = 0; - let sendPasswordResetEmailCalled = false; - const emailAdapter = { - sendVerificationEmail: () => Promise.resolve(), - sendPasswordResetEmail: () => { - sendPasswordResetEmailCalled = true; - }, - sendMail: () => {}, - }; - - await reconfigureServer({ - appName: 'test', - emailAdapter: emailAdapter, - publicServerURL: 'http://localhost:8378/1', + it('afterFind should not be triggered when saving an object', async () => { + let beforeSaves = 0; + Parse.Cloud.beforeSave('SavingTest', () => { + beforeSaves++; }); - Parse.Cloud.beforePasswordResetRequest(req => { - hit++; - if (req.object.get('isBanned')) { - throw new Error('banned account'); - } + let afterSaves = 0; + Parse.Cloud.afterSave('SavingTest', () => { + afterSaves++; }); - const user = new Parse.User(); - user.setUsername('banneduser'); - user.setPassword('password'); - user.set('email', 'banned@parse.com'); - await user.signUp(); - await user.save({ isBanned: true }); - - try { - await Parse.User.requestPasswordReset('banned@parse.com'); - throw new Error('should not have sent password reset email.'); - } catch (e) { - expect(e.message).toBe('banned account'); - } - expect(hit).toBe(1); - expect(sendPasswordResetEmailCalled).toBe(false); - done(); - }); - - it('should be able to block password reset request if an error is thrown even if the user has an attached file', async done => { - let hit = 0; - let sendPasswordResetEmailCalled = false; - const emailAdapter = { - sendVerificationEmail: () => Promise.resolve(), - sendPasswordResetEmail: () => { - sendPasswordResetEmailCalled = true; - }, - sendMail: () => {}, - }; - - await reconfigureServer({ - appName: 'test', - emailAdapter: emailAdapter, - publicServerURL: 'http://localhost:8378/1', + let beforeFinds = 0; + Parse.Cloud.beforeFind('SavingTest', () => { + beforeFinds++; }); - Parse.Cloud.beforePasswordResetRequest(req => { - hit++; - if (req.object.get('isBanned')) { - throw new Error('banned account'); - } + let afterFinds = 0; + Parse.Cloud.afterFind('SavingTest', () => { + afterFinds++; }); - const user = new Parse.User(); - user.setUsername('banneduser2'); - user.setPassword('password'); - user.set('email', 'banned2@parse.com'); - await user.signUp(); - const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE='; - const file = new Parse.File('myfile.txt', { base64 }); - await file.save(); - await user.save({ isBanned: true, file }); - - try { - await Parse.User.requestPasswordReset('banned2@parse.com'); - throw new Error('should not have sent password reset email.'); - } catch (e) { - expect(e.message).toBe('banned account'); - } - expect(hit).toBe(1); - expect(sendPasswordResetEmailCalled).toBe(false); - done(); - }); - - it('should not run beforePasswordResetRequest if email does not exist', async done => { - let hit = 0; - const emailAdapter = { - sendVerificationEmail: () => Promise.resolve(), - sendPasswordResetEmail: () => {}, - sendMail: () => {}, - }; - - await reconfigureServer({ - emailAdapter: emailAdapter, - publicServerURL: 'http://localhost:8378/1', - }); + const obj = new Parse.Object('SavingTest'); + obj.set('someField', 'some value 1'); + await obj.save(); - Parse.Cloud.beforePasswordResetRequest(req => { - hit++; - }); + expect(beforeSaves).toEqual(1); + expect(afterSaves).toEqual(1); + expect(beforeFinds).toEqual(0); + expect(afterFinds).toEqual(0); - try { - await Parse.User.requestPasswordReset('nonexistent@parse.com'); - } catch (e) { - // May or may not throw depending on passwordPolicy.resetPasswordSuccessOnInvalidEmail - } - expect(hit).toBe(0); - done(); - }); + obj.set('someField', 'some value 2'); + await obj.save(); - it('should have expected data in request in beforePasswordResetRequest', async done => { - const emailAdapter = { - sendVerificationEmail: () => Promise.resolve(), - sendPasswordResetEmail: () => {}, - sendMail: () => {}, - }; + expect(beforeSaves).toEqual(2); + expect(afterSaves).toEqual(2); + expect(beforeFinds).toEqual(0); + expect(afterFinds).toEqual(0); - await reconfigureServer({ - appName: 'test', - emailAdapter: emailAdapter, - publicServerURL: 'http://localhost:8378/1', - }); + await obj.fetch(); - Parse.Cloud.beforePasswordResetRequest(req => { - expect(req.object).toBeDefined(); - expect(req.object.get('email')).toBeDefined(); - expect(req.headers).toBeDefined(); - expect(req.ip).toBeDefined(); - expect(req.installationId).toBeDefined(); - expect(req.context).toBeDefined(); - expect(req.config).toBeDefined(); - }); + expect(beforeSaves).toEqual(2); + expect(afterSaves).toEqual(2); + expect(beforeFinds).toEqual(1); + expect(afterFinds).toEqual(1); - const user = new Parse.User(); - user.setUsername('testuser2'); - user.setPassword('password'); - user.set('email', 'test2@parse.com'); - await user.signUp(); - await Parse.User.requestPasswordReset('test2@parse.com'); - done(); - }); + obj.set('someField', 'some value 3'); + await obj.save(); - it('should validate that only _User class is allowed for beforePasswordResetRequest', () => { - expect(() => { - Parse.Cloud.beforePasswordResetRequest('SomeClass', () => { }); - }).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'); - expect(() => { - Parse.Cloud.beforePasswordResetRequest(() => { }); - }).not.toThrow(); - expect(() => { - Parse.Cloud.beforePasswordResetRequest('_User', () => { }); - }).not.toThrow(); - expect(() => { - Parse.Cloud.beforePasswordResetRequest(Parse.User, () => { }); - }).not.toThrow(); + expect(beforeSaves).toEqual(3); + expect(afterSaves).toEqual(3); + expect(beforeFinds).toEqual(1); + expect(afterFinds).toEqual(1); }); }); - - describe('afterLogin hook', () => { it('should run afterLogin after successful login', async done => { let hit = 0; @@ -4855,3 +4656,201 @@ describe('sendEmail', () => { ); }); }); + +describe('beforePasswordResetRequest hook', () => { + it('should run beforePasswordResetRequest with valid user', async done => { + let hit = 0; + let sendPasswordResetEmailCalled = false; + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => { + sendPasswordResetEmailCalled = true; + }, + sendMail: () => {}, + }; + + await reconfigureServer({ + appName: 'test', + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); + + Parse.Cloud.beforePasswordResetRequest(req => { + hit++; + expect(req.object).toBeDefined(); + expect(req.object.get('email')).toEqual('test@parse.com'); + expect(req.object.get('username')).toEqual('testuser'); + }); + + const user = new Parse.User(); + user.setUsername('testuser'); + user.setPassword('password'); + user.set('email', 'test@parse.com'); + await user.signUp(); + + await Parse.User.requestPasswordReset('test@parse.com'); + expect(hit).toBe(1); + expect(sendPasswordResetEmailCalled).toBe(true); + done(); + }); + + it('should be able to block password reset request if an error is thrown', async done => { + let hit = 0; + let sendPasswordResetEmailCalled = false; + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => { + sendPasswordResetEmailCalled = true; + }, + sendMail: () => {}, + }; + + await reconfigureServer({ + appName: 'test', + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); + + Parse.Cloud.beforePasswordResetRequest(req => { + hit++; + if (req.object.get('isBanned')) { + throw new Error('banned account'); + } + }); + + const user = new Parse.User(); + user.setUsername('banneduser'); + user.setPassword('password'); + user.set('email', 'banned@parse.com'); + await user.signUp(); + await user.save({ isBanned: true }); + + try { + await Parse.User.requestPasswordReset('banned@parse.com'); + throw new Error('should not have sent password reset email.'); + } catch (e) { + expect(e.message).toBe('banned account'); + } + expect(hit).toBe(1); + expect(sendPasswordResetEmailCalled).toBe(false); + done(); + }); + + it('should be able to block password reset request if an error is thrown even if the user has an attached file', async done => { + let hit = 0; + let sendPasswordResetEmailCalled = false; + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => { + sendPasswordResetEmailCalled = true; + }, + sendMail: () => {}, + }; + + await reconfigureServer({ + appName: 'test', + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); + + Parse.Cloud.beforePasswordResetRequest(req => { + hit++; + if (req.object.get('isBanned')) { + throw new Error('banned account'); + } + }); + + const user = new Parse.User(); + user.setUsername('banneduser2'); + user.setPassword('password'); + user.set('email', 'banned2@parse.com'); + await user.signUp(); + const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE='; + const file = new Parse.File('myfile.txt', { base64 }); + await file.save(); + await user.save({ isBanned: true, file }); + + try { + await Parse.User.requestPasswordReset('banned2@parse.com'); + throw new Error('should not have sent password reset email.'); + } catch (e) { + expect(e.message).toBe('banned account'); + } + expect(hit).toBe(1); + expect(sendPasswordResetEmailCalled).toBe(false); + done(); + }); + + it('should not run beforePasswordResetRequest if email does not exist', async done => { + let hit = 0; + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => {}, + sendMail: () => {}, + }; + + await reconfigureServer({ + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); + + Parse.Cloud.beforePasswordResetRequest(req => { + hit++; + }); + + try { + await Parse.User.requestPasswordReset('nonexistent@parse.com'); + } catch (e) { + // May or may not throw depending on passwordPolicy.resetPasswordSuccessOnInvalidEmail + } + expect(hit).toBe(0); + done(); + }); + + it('should have expected data in request in beforePasswordResetRequest', async done => { + const emailAdapter = { + sendVerificationEmail: () => Promise.resolve(), + sendPasswordResetEmail: () => {}, + sendMail: () => {}, + }; + + await reconfigureServer({ + appName: 'test', + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }); + + Parse.Cloud.beforePasswordResetRequest(req => { + expect(req.object).toBeDefined(); + expect(req.object.get('email')).toBeDefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + expect(req.config).toBeDefined(); + }); + + const user = new Parse.User(); + user.setUsername('testuser2'); + user.setPassword('password'); + user.set('email', 'test2@parse.com'); + await user.signUp(); + await Parse.User.requestPasswordReset('test2@parse.com'); + done(); + }); + + it('should validate that only _User class is allowed for beforePasswordResetRequest', () => { + expect(() => { + Parse.Cloud.beforePasswordResetRequest('SomeClass', () => { }); + }).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers'); + expect(() => { + Parse.Cloud.beforePasswordResetRequest(() => { }); + }).not.toThrow(); + expect(() => { + Parse.Cloud.beforePasswordResetRequest('_User', () => { }); + }).not.toThrow(); + expect(() => { + Parse.Cloud.beforePasswordResetRequest(Parse.User, () => { }); + }).not.toThrow(); + }); +});