diff --git a/CHANGELOG.md b/CHANGELOG.md index 19e4e61f1c..cd3ae53199 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -158,6 +158,7 @@ ___ - Allow setting descending sort to full text queries (dblythy) [#7496](https://github.com/parse-community/parse-server/pull/7496) - Allow cloud string for ES modules (Daniel Blyth) [#7560](https://github.com/parse-community/parse-server/pull/7560) - docs: Introduce deprecation ID for reference in comments and online search (Manuel Trezza) [#7562](https://github.com/parse-community/parse-server/pull/7562) +- refactor: simplify Cloud Code tests and FunctionsRouter (Daniel Blyth) [#7564](https://github.com/parse-community/parse-server/pull/7564) - refactor: Modernize HTTPRequest tests (brandongregoryscott) [#7604](https://github.com/parse-community/parse-server/pull/7604) - Allow liveQuery on Session class (Daniel Blyth) [#7554](https://github.com/parse-community/parse-server/pull/7554) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index adace31078..ec6a6a3215 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -19,24 +19,40 @@ const mockAdapter = { }; describe('Cloud Code', () => { - it('can load absolute cloud code file', done => { - reconfigureServer({ - cloud: __dirname + '/cloud/cloudCodeRelativeFile.js', - }).then(() => { - Parse.Cloud.run('cloudCodeInFile', {}).then(result => { - expect(result).toEqual('It is possible to define cloud code in a file.'); - done(); - }); + it('can load absolute cloud code file', async () => { + await reconfigureServer({ + cloud: `${__dirname}/cloud/cloudCodeRelativeFile.js`, }); + await expectAsync(Parse.Cloud.run('cloudCodeInFile')).toBeResolvedTo( + 'It is possible to define cloud code in a file.' + ); }); - it('can load relative cloud code file', done => { - reconfigureServer({ cloud: './spec/cloud/cloudCodeAbsoluteFile.js' }).then(() => { - Parse.Cloud.run('cloudCodeInFile', {}).then(result => { - expect(result).toEqual('It is possible to define cloud code in a file.'); - done(); - }); + it('can load relative cloud code file', async () => { + await reconfigureServer({ cloud: './spec/cloud/cloudCodeAbsoluteFile.js' }); + await expectAsync(Parse.Cloud.run('cloudCodeInFile')).toBeResolvedTo( + 'It is possible to define cloud code in a file.' + ); + }); + + it('can create functions', async () => { + Parse.Cloud.define('hello', () => 'Hello world!'); + await expectAsync(Parse.Cloud.run('hello')).toBeResolvedTo('Hello world!'); + }); + + it('can load cloud code from function', async () => { + reconfigureServer({ + cloud: () => { + Parse.Cloud.define('hello', () => 'Hello world...'); + }, }); + await expectAsync(Parse.Cloud.run('hello')).toBeResolvedTo('Hello world...'); + }); + + it('cloud code should be a function or string', async () => { + await expectAsync(reconfigureServer({ cloud: [] })).toBeRejectedWith( + "argument 'cloud' must either be a string or a function" + ); }); it('can load cloud code as a module', async () => { @@ -47,58 +63,32 @@ describe('Cloud Code', () => { delete process.env.npm_package_type; }); - it('can create functions', done => { - Parse.Cloud.define('hello', () => { - return 'Hello world!'; - }); - - Parse.Cloud.run('hello', {}).then(result => { - expect(result).toEqual('Hello world!'); - done(); - }); - }); - - it('show warning on duplicate cloud functions', done => { - const logger = require('../lib/logger').logger; + it('show warning on duplicate cloud functions', () => { + const { logger } = require('../lib/logger'); spyOn(logger, 'warn').and.callFake(() => {}); - Parse.Cloud.define('hello', () => { - return 'Hello world!'; - }); - Parse.Cloud.define('hello', () => { - return 'Hello world!'; - }); + Parse.Cloud.define('hello', () => 'Hello world!'); + Parse.Cloud.define('hello', () => 'Hello world!'); expect(logger.warn).toHaveBeenCalledWith( 'Warning: Duplicate cloud functions exist for hello. Only the last one will be used and the others will be ignored.' ); - done(); }); - it('is cleared cleared after the previous test', done => { - Parse.Cloud.run('hello', {}).catch(error => { - expect(error.code).toEqual(141); - done(); - }); + it('is cleared cleared after the previous test', async () => { + await expectAsync(Parse.Cloud.run('hello')).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, `Invalid function: "hello"`) + ); }); - it('basic beforeSave rejection', function (done) { - Parse.Cloud.beforeSave('BeforeSaveFail', function () { + it('basic beforeSave rejection', async () => { + Parse.Cloud.beforeSave('BeforeSaveFail', () => { throw new Error('You shall not pass!'); }); - - const obj = new Parse.Object('BeforeSaveFail'); - obj.set('foo', 'bar'); - obj.save().then( - () => { - fail('Should not have been able to save BeforeSaveFailure class.'); - done(); - }, - () => { - done(); - } + await expectAsync(new Parse.Object('BeforeSaveFail').save()).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, `You shall not pass!`) ); }); - it('returns an error', done => { + it('returns an error', async () => { Parse.Cloud.define('cloudCodeWithError', () => { /* eslint-disable no-undef */ foo.bar(); @@ -106,145 +96,77 @@ describe('Cloud Code', () => { return 'I better throw an error.'; }); - Parse.Cloud.run('cloudCodeWithError').then( - () => done.fail('should not succeed'), - e => { - expect(e).toEqual(new Parse.Error(141, 'foo is not defined')); - done(); - } + await expectAsync(Parse.Cloud.run('cloudCodeWithError')).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'foo is not defined') ); }); - it('returns an empty error', done => { + it('returns an empty error', async () => { Parse.Cloud.define('cloudCodeWithError', () => { throw null; }); - - Parse.Cloud.run('cloudCodeWithError').then( - () => done.fail('should not succeed'), - e => { - expect(e.code).toEqual(141); - expect(e.message).toEqual('Script failed.'); - done(); - } + await expectAsync(Parse.Cloud.run('cloudCodeWithError')).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Script failed.') ); }); - it('beforeFind can throw string', async function (done) { + it('beforeFind can throw string', async () => { Parse.Cloud.beforeFind('beforeFind', () => { throw 'throw beforeFind'; }); - const obj = new Parse.Object('beforeFind'); - obj.set('foo', 'bar'); - await obj.save(); - expect(obj.get('foo')).toBe('bar'); - try { - const query = new Parse.Query('beforeFind'); - await query.first(); - } catch (e) { - expect(e.code).toBe(141); - expect(e.message).toBe('throw beforeFind'); - done(); - } + await expectAsync(new Parse.Query('beforeFind').first()).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'throw beforeFind') + ); }); - it('beforeSave rejection with custom error code', function (done) { - Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () { - throw new Parse.Error(999, 'Nope'); + it('beforeSave rejection with custom error code', async () => { + const error = new Parse.Error(999, 'Nope'); + Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', () => { + throw error; }); - - const obj = new Parse.Object('BeforeSaveFailWithErrorCode'); - obj.set('foo', 'bar'); - obj.save().then( - function () { - fail('Should not have been able to save BeforeSaveFailWithErrorCode class.'); - done(); - }, - function (error) { - expect(error.code).toEqual(999); - expect(error.message).toEqual('Nope'); - done(); - } + await expectAsync(new Parse.Object('BeforeSaveFailWithErrorCode').save()).toBeRejectedWith( + error ); }); - it('basic beforeSave rejection via promise', function (done) { - Parse.Cloud.beforeSave('BeforeSaveFailWithPromise', function () { + it('basic beforeSave rejection via promise', async () => { + Parse.Cloud.beforeSave('BeforeSaveFailWithPromise', async () => { const query = new Parse.Query('Yolo'); - return query.find().then( - () => { - throw 'Nope'; - }, - () => { - return Promise.response(); - } - ); + await query.find(); + throw 'Nope'; }); - - const obj = new Parse.Object('BeforeSaveFailWithPromise'); - obj.set('foo', 'bar'); - obj.save().then( - function () { - fail('Should not have been able to save BeforeSaveFailure class.'); - done(); - }, - function (error) { - expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); - expect(error.message).toEqual('Nope'); - done(); - } + await expectAsync(new Parse.Object('BeforeSaveFailWithPromise').save()).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Nope') ); }); - it('test beforeSave changed object success', function (done) { - Parse.Cloud.beforeSave('BeforeSaveChanged', function (req) { - req.object.set('foo', 'baz'); + it('test beforeSave changed object success', async () => { + Parse.Cloud.beforeSave('BeforeSaveChanged', ({ object }) => { + object.set('foo', 'baz'); }); const obj = new Parse.Object('BeforeSaveChanged'); obj.set('foo', 'bar'); - obj.save().then( - function () { - const query = new Parse.Query('BeforeSaveChanged'); - query.get(obj.id).then( - function (objAgain) { - expect(objAgain.get('foo')).toEqual('baz'); - done(); - }, - function (error) { - fail(error); - done(); - } - ); - }, - function (error) { - fail(error); - done(); - } - ); + await obj.save(); + expect(obj.get('foo')).toBe('baz'); + const objAgain = await new Parse.Query('BeforeSaveChanged').get(obj.id); + expect(objAgain.get('foo')).toBe('baz'); }); it('test beforeSave with invalid field', async () => { Parse.Cloud.beforeSave('BeforeSaveChanged', function (req) { req.object.set('length', 0); }); - - const obj = new Parse.Object('BeforeSaveChanged'); - obj.set('foo', 'bar'); - try { - await obj.save(); - fail('should not succeed'); - } catch (e) { - expect(e.message).toBe('Invalid field name: length.'); - } + await expectAsync(new Parse.Object('BeforeSaveChanged').save()).toBeRejectedWith( + new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid field name: length.') + ); }); - it("test beforeSave changed object fail doesn't change object", async function () { - Parse.Cloud.beforeSave('BeforeSaveChanged', function (req) { + it("test beforeSave changed object fail doesn't change object", async () => { + Parse.Cloud.beforeSave('BeforeSaveChanged', req => { if (req.object.has('fail')) { return Promise.reject(new Error('something went wrong')); } - return Promise.resolve(); }); @@ -252,47 +174,40 @@ describe('Cloud Code', () => { obj.set('foo', 'bar'); await obj.save(); obj.set('foo', 'baz').set('fail', true); - try { - await obj.save(); - } catch (e) { - await obj.fetch(); - expect(obj.get('foo')).toBe('bar'); - } + await expectAsync(obj.save()).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'something went wrong') + ); + await obj.fetch(); + expect(obj.get('foo')).toBe('bar'); }); - it('test beforeSave returns value on create and update', done => { - Parse.Cloud.beforeSave('BeforeSaveChanged', function (req) { + it('test beforeSave returns value on create and update', async () => { + Parse.Cloud.beforeSave('BeforeSaveChanged', req => { req.object.set('foo', 'baz'); }); - const obj = new Parse.Object('BeforeSaveChanged'); obj.set('foo', 'bing'); - obj.save().then(() => { - expect(obj.get('foo')).toEqual('baz'); - obj.set('foo', 'bar'); - return obj.save().then(() => { - expect(obj.get('foo')).toEqual('baz'); - done(); - }); - }); + await obj.save(); + expect(obj.get('foo')).toEqual('baz'); + obj.set('foo', 'bar'); + await obj.save(); + expect(obj.get('foo')).toEqual('baz'); }); - it('test beforeSave applies changes when beforeSave returns true', done => { - Parse.Cloud.beforeSave('Insurance', function (req) { + it('test beforeSave applies changes when beforeSave returns true', async () => { + Parse.Cloud.beforeSave('Insurance', req => { req.object.set('rate', '$49.99/Month'); return true; }); const insurance = new Parse.Object('Insurance'); insurance.set('rate', '$5.00/Month'); - insurance.save().then(insurance => { - expect(insurance.get('rate')).toEqual('$49.99/Month'); - done(); - }); + await insurance.save(); + expect(insurance.get('rate')).toEqual('$49.99/Month'); }); - it('test beforeSave applies changes and resolves returned promise', done => { - Parse.Cloud.beforeSave('Insurance', function (req) { + it('test beforeSave applies changes and resolves returned promise', async () => { + Parse.Cloud.beforeSave('Insurance', req => { req.object.set('rate', '$49.99/Month'); return new Parse.Query('Pet').get(req.object.get('pet').id).then(pet => { pet.set('healthy', true); @@ -302,45 +217,26 @@ describe('Cloud Code', () => { const pet = new Parse.Object('Pet'); pet.set('healthy', false); - pet.save().then(pet => { - const insurance = new Parse.Object('Insurance'); - insurance.set('pet', pet); - insurance.set('rate', '$5.00/Month'); - insurance.save().then(insurance => { - expect(insurance.get('rate')).toEqual('$49.99/Month'); - new Parse.Query('Pet').get(insurance.get('pet').id).then(pet => { - expect(pet.get('healthy')).toEqual(true); - done(); - }); - }); - }); + await pet.save(); + const insurance = new Parse.Object('Insurance'); + insurance.set('pet', pet); + insurance.set('rate', '$5.00/Month'); + await insurance.save(); + expect(insurance.get('rate')).toEqual('$49.99/Month'); + const _pet = await new Parse.Query('Pet').get(insurance.get('pet').id); + expect(_pet.get('healthy')).toEqual(true); }); it('beforeSave should be called only if user fulfills permissions', async () => { - const triggeruser = new Parse.User(); - triggeruser.setUsername('triggeruser'); - triggeruser.setPassword('triggeruser'); - await triggeruser.signUp(); - - const triggeruser2 = new Parse.User(); - triggeruser2.setUsername('triggeruser2'); - triggeruser2.setPassword('triggeruser2'); - await triggeruser2.signUp(); - - const triggeruser3 = new Parse.User(); - triggeruser3.setUsername('triggeruser3'); - triggeruser3.setPassword('triggeruser3'); - await triggeruser3.signUp(); - - const triggeruser4 = new Parse.User(); - triggeruser4.setUsername('triggeruser4'); - triggeruser4.setPassword('triggeruser4'); - await triggeruser4.signUp(); - - const triggeruser5 = new Parse.User(); - triggeruser5.setUsername('triggeruser5'); - triggeruser5.setPassword('triggeruser5'); - await triggeruser5.signUp(); + const [triggeruser, triggeruser2, triggeruser3, triggeruser4, triggeruser5] = await Promise.all( + [1, 2, 3, 4, 5].map(async num => { + const user = new Parse.User(); + user.setUsername(`triggeruser${num}`); + user.setPassword(`triggeruser${num}`); + await user.signUp(); + return user; + }) + ); const triggerroleacl = new Parse.ACL(); triggerroleacl.setPublicReadAccess(true); @@ -397,11 +293,11 @@ describe('Cloud Code', () => { {} ); - let called = 0; - Parse.Cloud.beforeSave('triggerclass', () => { - called++; - }); - + const caller = { + beforeSave: () => {}, + }; + const spy = spyOn(caller, 'beforeSave').and.callThrough(); + Parse.Cloud.beforeSave('triggerclass', caller.beforeSave); const triggerobject = new Parse.Object('triggerclass'); triggerobject.set('someField', 'someValue'); triggerobject.set('someField2', 'someValue'); @@ -419,19 +315,19 @@ describe('Cloud Code', () => { await triggerobject.save(undefined, { sessionToken: triggeruser.getSessionToken(), }); - expect(called).toBe(1); + expect(spy).toHaveBeenCalledTimes(1); await triggerobject.save(undefined, { sessionToken: triggeruser.getSessionToken(), }); - expect(called).toBe(2); + expect(spy).toHaveBeenCalledTimes(2); await triggerobject.save(undefined, { sessionToken: triggeruser2.getSessionToken(), }); - expect(called).toBe(3); + expect(spy).toHaveBeenCalledTimes(3); await triggerobject.save(undefined, { sessionToken: triggeruser3.getSessionToken(), }); - expect(called).toBe(4); + expect(spy).toHaveBeenCalledTimes(4); const triggerobject2 = new Parse.Object('triggerclass'); triggerobject2.set('someField', 'someValue'); @@ -450,109 +346,89 @@ describe('Cloud Code', () => { await triggerobject2.save(undefined, { sessionToken: triggeruser2.getSessionToken(), }); - expect(called).toBe(5); + expect(spy).toHaveBeenCalledTimes(5); await triggerobject2.save(undefined, { sessionToken: triggeruser2.getSessionToken(), }); - expect(called).toBe(6); + expect(spy).toHaveBeenCalledTimes(6); await triggerobject2.save(undefined, { sessionToken: triggeruser.getSessionToken(), }); - expect(called).toBe(7); + expect(spy).toHaveBeenCalledTimes(7); - let catched = false; - try { - await triggerobject2.save(undefined, { + await expectAsync( + triggerobject2.save(undefined, { sessionToken: triggeruser3.getSessionToken(), - }); - } catch (e) { - catched = true; - expect(e.code).toBe(101); - } - expect(catched).toBe(true); - expect(called).toBe(7); + }) + ).toBeRejectedWith(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.')); - catched = false; - try { - await triggerobject2.save(undefined, { + expect(spy).toHaveBeenCalledTimes(7); + + await expectAsync( + triggerobject2.save(undefined, { sessionToken: triggeruser4.getSessionToken(), - }); - } catch (e) { - catched = true; - expect(e.code).toBe(101); - } - expect(catched).toBe(true); - expect(called).toBe(7); + }) + ).toBeRejectedWith(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.')); - catched = false; - try { - await triggerobject2.save(undefined, { + expect(spy).toHaveBeenCalledTimes(7); + + await expectAsync( + triggerobject2.save(undefined, { sessionToken: triggeruser5.getSessionToken(), - }); - } catch (e) { - catched = true; - expect(e.code).toBe(101); - } - expect(catched).toBe(true); - expect(called).toBe(7); + }) + ).toBeRejectedWith(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.')); + + expect(spy).toHaveBeenCalledTimes(7); const triggerobject3 = new Parse.Object('triggerclass'); triggerobject3.set('someField', 'someValue'); triggerobject3.set('someField33', 'someValue'); - catched = false; - try { - await triggerobject3.save(undefined, { + await expectAsync( + triggerobject3.save(undefined, { sessionToken: triggeruser4.getSessionToken(), - }); - } catch (e) { - catched = true; - expect(e.code).toBe(119); - } - expect(catched).toBe(true); - expect(called).toBe(7); + }) + ).toBeRejectedWith( + new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + 'Permission denied for action create on class triggerclass.' + ) + ); - catched = false; - try { - await triggerobject3.save(undefined, { + expect(spy).toHaveBeenCalledTimes(7); + + await expectAsync( + triggerobject3.save(undefined, { sessionToken: triggeruser5.getSessionToken(), - }); - } catch (e) { - catched = true; - expect(e.code).toBe(119); - } - expect(catched).toBe(true); - expect(called).toBe(7); + }) + ).toBeRejectedWith( + new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + 'Permission denied for action create on class triggerclass.' + ) + ); + expect(spy).toHaveBeenCalledTimes(7); }); - it('test afterSave ran and created an object', function (done) { - Parse.Cloud.afterSave('AfterSaveTest', function (req) { + it('test afterSave ran and created an object', done => { + const obj = new Parse.Object('AfterSaveTest'); + const test = async () => { + const query = new Parse.Query('AfterSaveProof'); + query.equalTo('proof', obj.id); + const results = await query.find(); + expect(results.length).toEqual(1); + done(); + }; + Parse.Cloud.afterSave('AfterSaveTest', req => { const obj = new Parse.Object('AfterSaveProof'); obj.set('proof', req.object.id); obj.save().then(test); }); - - const obj = new Parse.Object('AfterSaveTest'); obj.save(); - - function test() { - const query = new Parse.Query('AfterSaveProof'); - query.equalTo('proof', obj.id); - query.find().then( - function (results) { - expect(results.length).toEqual(1); - done(); - }, - function (error) { - fail(error); - done(); - } - ); - } }); - it('test afterSave ran on created object and returned a promise', function (done) { - Parse.Cloud.afterSave('AfterSaveTest2', function (req) { + it('test afterSave ran on created object and returned a promise', async () => { + Parse.Cloud.afterSave('AfterSaveTest2', req => { const obj = req.object; if (!obj.existed()) { return new Promise(resolve => { @@ -566,23 +442,11 @@ describe('Cloud Code', () => { } }); - const obj = new Parse.Object('AfterSaveTest2'); - obj.save().then(function () { - const query = new Parse.Query('AfterSaveTest2'); - query.equalTo('proof', obj.id); - query.find().then( - function (results) { - expect(results.length).toEqual(1); - const savedObject = results[0]; - expect(savedObject.get('proof')).toEqual(obj.id); - done(); - }, - function (error) { - fail(error); - done(); - } - ); - }); + const obj = await new Parse.Object('AfterSaveTest2').save(); + const results = await new Parse.Query('AfterSaveTest2').equalTo('proof', obj.id).find(); + expect(results.length).toEqual(1); + const [savedObject] = results; + expect(savedObject.get('proof')).toEqual(obj.id); }); // TODO: Fails on CI randomly as racing @@ -618,28 +482,18 @@ describe('Cloud Code', () => { ); }); - it('test afterSave rejecting promise', function (done) { - Parse.Cloud.afterSave('AfterSaveTest2', function () { + it('test afterSave rejecting promise', async () => { + Parse.Cloud.afterSave('AfterSaveTest2', () => { return new Promise((resolve, reject) => { setTimeout(function () { reject('THIS SHOULD BE IGNORED'); }, 1000); }); }); - - const obj = new Parse.Object('AfterSaveTest2'); - obj.save().then( - function () { - done(); - }, - function (error) { - fail(error); - done(); - } - ); + await new Parse.Object('AfterSaveTest2').save(); }); - it('test afterDelete returning promise, object is deleted when destroy resolves', function (done) { + it('test afterDelete returning promise, object is deleted when destroy resolves', async () => { Parse.Cloud.afterDelete('AfterDeleteTest2', function (req) { return new Promise(resolve => { setTimeout(function () { @@ -652,28 +506,17 @@ describe('Cloud Code', () => { }); }); - const errorHandler = function (error) { - fail(error); - done(); - }; - const obj = new Parse.Object('AfterDeleteTest2'); - obj.save().then(function () { - obj.destroy().then(function () { - const query = new Parse.Query('AfterDeleteTestProof'); - query.equalTo('proof', obj.id); - query.find().then(function (results) { - expect(results.length).toEqual(1); - const deletedObject = results[0]; - expect(deletedObject.get('proof')).toEqual(obj.id); - done(); - }, errorHandler); - }, errorHandler); - }, errorHandler); + await obj.save(); + await obj.destroy(); + const results = await new Parse.Query('AfterDeleteTestProof').equalTo('proof', obj.id).find(); + expect(results.length).toEqual(1); + const [deletedObject] = results; + expect(deletedObject.get('proof')).toEqual(obj.id); }); - it('test afterDelete ignoring promise, object is not yet deleted', function (done) { - Parse.Cloud.afterDelete('AfterDeleteTest2', function (req) { + it('test afterDelete ignoring promise, object is not yet deleted', async () => { + Parse.Cloud.afterDelete('AfterDeleteTest2', req => { return new Promise(resolve => { setTimeout(function () { const obj = new Parse.Object('AfterDeleteTestProof'); @@ -685,148 +528,82 @@ describe('Cloud Code', () => { }); }); - const errorHandler = function (error) { - fail(error); - done(); - }; - const obj = new Parse.Object('AfterDeleteTest2'); - obj.save().then(function () { - obj.destroy().then(function () { - done(); - }); + await obj.save(); + const query = new Parse.Query('AfterDeleteTestProof'); + query.equalTo('proof', obj.id); + const [results] = await Promise.all([query.find(), obj.destroy()]); - const query = new Parse.Query('AfterDeleteTestProof'); - query.equalTo('proof', obj.id); - query.find().then(function (results) { - expect(results.length).toEqual(0); - }, errorHandler); - }, errorHandler); + expect(results.length).toEqual(0); }); - it('test beforeSave happens on update', function (done) { - Parse.Cloud.beforeSave('BeforeSaveChanged', function (req) { + it('test beforeSave happens on update', async () => { + Parse.Cloud.beforeSave('BeforeSaveChanged', req => { req.object.set('foo', 'baz'); }); const obj = new Parse.Object('BeforeSaveChanged'); obj.set('foo', 'bar'); - obj - .save() - .then(function () { - obj.set('foo', 'bar'); - return obj.save(); - }) - .then( - function () { - const query = new Parse.Query('BeforeSaveChanged'); - return query.get(obj.id).then(function (objAgain) { - expect(objAgain.get('foo')).toEqual('baz'); - done(); - }); - }, - function (error) { - fail(error); - done(); - } - ); + await obj.save(); + expect(obj.get('foo')).toBe('baz'); + obj.set('foo', 'bar'); + await obj.save(); + obj.set('foo', 'bar'); + const objAgain = await new Parse.Query('BeforeSaveChanged').get(obj.id); + expect(objAgain.get('foo')).toEqual('baz'); }); - it('test beforeDelete failure', function (done) { - Parse.Cloud.beforeDelete('BeforeDeleteFail', function () { + it('test beforeDelete failure', async () => { + Parse.Cloud.beforeDelete('BeforeDeleteFail', () => { throw 'Nope'; }); const obj = new Parse.Object('BeforeDeleteFail'); - let id; obj.set('foo', 'bar'); - obj - .save() - .then(() => { - id = obj.id; - return obj.destroy(); - }) - .then( - () => { - fail('obj.destroy() should have failed, but it succeeded'); - done(); - }, - error => { - expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); - expect(error.message).toEqual('Nope'); + await obj.save(); + const id = `${obj.id}`; + await expectAsync(obj.destroy()).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Nope') + ); - const objAgain = new Parse.Object('BeforeDeleteFail', { - objectId: id, - }); - return objAgain.fetch(); - } - ) - .then( - objAgain => { - if (objAgain) { - expect(objAgain.get('foo')).toEqual('bar'); - } else { - fail('unable to fetch the object ', id); - } - done(); - }, - error => { - // We should have been able to fetch the object again - fail(error); - } - ); + const objAgain = new Parse.Object('BeforeDeleteFail', { + objectId: id, + }); + await objAgain.fetch(); + expect(objAgain.get('foo')).toEqual('bar'); }); - it('basic beforeDelete rejection via promise', function (done) { - Parse.Cloud.beforeSave('BeforeDeleteFailWithPromise', function () { + it('basic beforeDelete rejection via promise', async () => { + Parse.Cloud.beforeSave('BeforeDeleteFailWithPromise', () => { const query = new Parse.Query('Yolo'); return query.find().then(() => { throw 'Nope'; }); }); - const obj = new Parse.Object('BeforeDeleteFailWithPromise'); obj.set('foo', 'bar'); - obj.save().then( - function () { - fail('Should not have been able to save BeforeSaveFailure class.'); - done(); - }, - function (error) { - expect(error.code).toEqual(Parse.Error.SCRIPT_FAILED); - expect(error.message).toEqual('Nope'); - - done(); - } + await expectAsync(obj.save()).toBeRejectedWith( + new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Nope') ); }); - it('test afterDelete ran and created an object', function (done) { + it('test afterDelete ran and created an object', async done => { + const obj = new Parse.Object('AfterDeleteTest'); + const test = async () => { + const query = new Parse.Query('AfterDeleteProof'); + query.equalTo('proof', obj.id); + const results = await query.find(); + expect(results.length).toEqual(1); + done(); + }; Parse.Cloud.afterDelete('AfterDeleteTest', function (req) { const obj = new Parse.Object('AfterDeleteProof'); obj.set('proof', req.object.id); obj.save().then(test); }); - const obj = new Parse.Object('AfterDeleteTest'); - obj.save().then(function () { - obj.destroy(); - }); - - function test() { - const query = new Parse.Query('AfterDeleteProof'); - query.equalTo('proof', obj.id); - query.find().then( - function (results) { - expect(results.length).toEqual(1); - done(); - }, - function (error) { - fail(error); - done(); - } - ); - } + await obj.save(); + await obj.destroy(); }); it('test cloud function return types', function (done) { @@ -1584,6 +1361,19 @@ describe('Cloud Code', () => { .catch(done.fail); }); + it('run job should fail', async () => { + await expectAsync( + request({ + method: 'POST', + url: 'http://localhost:8378/1/jobs/failedJob', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + }, + }) + ).toBeRejected(); + }); + it('should not run without master key', done => { expect(() => { Parse.Cloud.job('myJob', () => {}); @@ -1641,6 +1431,42 @@ describe('Cloud Code', () => { .catch(done.fail); }); + it('should run a job as body param', done => { + expect(() => { + Parse.Cloud.job('myJob', (req, res) => { + expect(req.functionName).toBeUndefined(); + expect(req.jobName).toBe('myJob'); + expect(typeof req.jobId).toBe('string'); + expect(typeof req.message).toBe('function'); + expect(typeof res).toBe('undefined'); + }); + }).not.toThrow(); + + request({ + method: 'POST', + url: 'http://localhost:8378/1/jobs', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + }, + body: { + jobName: 'myJob', + }, + }) + .then(async response => { + const jobStatusId = response.headers['x-parse-job-status-id']; + const checkJobStatus = async () => { + const jobStatus = await getJobStatus(jobStatusId); + return jobStatus.get('finishedAt'); + }; + while (!(await checkJobStatus())) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + }) + .then(done) + .catch(done.fail); + }); + it('should run with master key basic auth', done => { expect(() => { Parse.Cloud.job('myJob', (req, res) => { diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index d239908103..7cce0152d2 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -1,8 +1,7 @@ // FunctionsRouter.js -var Parse = require('parse/node').Parse, - triggers = require('../triggers'); - +import Parse from 'parse/node'; +import { getJob, getFunction, maybeRunValidator, resolveError } from '../triggers.js'; import PromiseRouter from '../PromiseRouter'; import { promiseEnforceMasterKeyAccess, promiseEnsureIdempotency } from '../middlewares'; import { jobStatusHandler } from '../StatusHandler'; @@ -10,19 +9,22 @@ import _ from 'lodash'; import { logger } from '../logger'; function parseObject(obj) { + if (!(obj ?? false)) { + return obj; + } if (Array.isArray(obj)) { - return obj.map(item => { - return parseObject(item); - }); - } else if (obj && obj.__type == 'Date') { + return obj.map(item => parseObject(item)); + } + if (obj.__type === 'Date') { return Object.assign(new Date(obj.iso), obj); - } else if (obj && obj.__type == 'File') { + } + if (obj.__type === 'File') { return Parse.File.fromJSON(obj); - } else if (obj && typeof obj === 'object') { + } + if (typeof obj === 'object') { return parseParams(obj); - } else { - return obj; } + return obj; } function parseParams(params) { @@ -42,27 +44,22 @@ export class FunctionsRouter extends PromiseRouter { '/jobs/:jobName', promiseEnsureIdempotency, promiseEnforceMasterKeyAccess, - function (req) { - return FunctionsRouter.handleCloudJob(req); - } + FunctionsRouter.handleCloudJob ); - this.route('POST', '/jobs', promiseEnforceMasterKeyAccess, function (req) { - return FunctionsRouter.handleCloudJob(req); - }); + this.route('POST', '/jobs', promiseEnforceMasterKeyAccess, FunctionsRouter.handleCloudJob); } - static handleCloudJob(req) { + static async handleCloudJob(req) { const jobName = req.params.jobName || req.body.jobName; const applicationId = req.config.applicationId; const jobHandler = jobStatusHandler(req.config); - const jobFunction = triggers.getJob(jobName, applicationId); + const jobFunction = getJob(jobName, applicationId); if (!jobFunction) { throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid job.'); } - let params = Object.assign({}, req.body, req.query); - params = parseParams(params); + const params = parseParams({ ...req.body, ...req.query }); const request = { - params: params, + params, log: req.config.loggerController, headers: req.config.headers, ip: req.config.ip, @@ -70,61 +67,40 @@ export class FunctionsRouter extends PromiseRouter { message: jobHandler.setMessage.bind(jobHandler), }; - return jobHandler.setRunning(jobName, params).then(jobStatus => { - request.jobId = jobStatus.objectId; - // run the function async - process.nextTick(() => { - Promise.resolve() - .then(() => { - return jobFunction(request); - }) - .then( - result => { - jobHandler.setSucceeded(result); - }, - error => { - jobHandler.setFailed(error); - } - ); - }); - return { - headers: { - 'X-Parse-Job-Status-Id': jobStatus.objectId, - }, - response: {}, - }; + const jobStatus = await jobHandler.setRunning(jobName, params); + request.jobId = jobStatus.objectId; + // run the function async + process.nextTick(() => { + (async () => { + try { + const result = await jobFunction(request); + jobHandler.setSucceeded(result); + } catch (error) { + jobHandler.setFailed(error); + } + })(); }); - } - - static createResponseObject(resolve, reject) { return { - success: function (result) { - resolve({ - response: { - result: Parse._encode(result), - }, - }); - }, - error: function (message) { - const error = triggers.resolveError(message); - reject(error); + headers: { + 'X-Parse-Job-Status-Id': jobStatus.objectId, }, + response: {}, }; } - static handleCloudFunction(req) { + + static async handleCloudFunction(req) { const functionName = req.params.functionName; const applicationId = req.config.applicationId; - const theFunction = triggers.getFunction(functionName, applicationId); + const theFunction = getFunction(functionName, applicationId); if (!theFunction) { throw new Parse.Error(Parse.Error.SCRIPT_FAILED, `Invalid function: "${functionName}"`); } - let params = Object.assign({}, req.body, req.query); - params = parseParams(params); + const params = parseParams({ ...req.body, ...req.query }); const request = { - params: params, - master: req.auth && req.auth.isMaster, - user: req.auth && req.auth.user, + params, + master: req.auth?.isMaster, + user: req.auth?.user, installationId: req.info.installationId, log: req.config.loggerController, headers: req.config.headers, @@ -133,52 +109,34 @@ export class FunctionsRouter extends PromiseRouter { context: req.info.context, }; - return new Promise(function (resolve, reject) { - const userString = req.auth && req.auth.user ? req.auth.user.id : undefined; - const cleanInput = logger.truncateLogMessage(JSON.stringify(params)); - const { success, error } = FunctionsRouter.createResponseObject( - result => { - try { - const cleanResult = logger.truncateLogMessage(JSON.stringify(result.response.result)); - logger.info( - `Ran cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Result: ${cleanResult}`, - { - functionName, - params, - user: userString, - } - ); - resolve(result); - } catch (e) { - reject(e); - } - }, - error => { - try { - logger.error( - `Failed running cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Error: ` + - JSON.stringify(error), - { - functionName, - error, - params, - user: userString, - } - ); - reject(error); - } catch (e) { - reject(e); - } + const userString = req.auth?.user?.id; + const cleanInput = logger.truncateLogMessage(JSON.stringify(params)); + try { + await maybeRunValidator(request, functionName, req.auth); + const response = await theFunction(request); + const result = Parse._encode(response); + const cleanResult = logger.truncateLogMessage(JSON.stringify(result)); + logger.info( + `Ran cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Result: ${cleanResult}`, + { + functionName, + params, + user: userString, } ); - return Promise.resolve() - .then(() => { - return triggers.maybeRunValidator(request, functionName, req.auth); - }) - .then(() => { - return theFunction(request); - }) - .then(success, error); - }); + return { + response: { + result, + }, + }; + } catch (e) { + const error = resolveError(e); + logger.error( + `Failed running cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Error: ${JSON.stringify( + error + )}\n Stack: ${error.stack}\n` + ); + throw error; + } } }