diff --git a/spec/.eslintrc.json b/spec/.eslintrc.json index 0814f305ce..f9f4085fa8 100644 --- a/spec/.eslintrc.json +++ b/spec/.eslintrc.json @@ -23,7 +23,8 @@ "range": true, "jequal": true, "create": true, - "arrayContains": true + "arrayContains": true, + "delay": true }, "rules": { "no-console": [0], diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index e02c4cc3ca..18b7256543 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1950,11 +1950,11 @@ describe('afterFind hooks', () => { it('should validate triggers correctly', () => { expect(() => { - Parse.Cloud.beforeSave('_Session', () => {}); - }).toThrow('Triggers are not supported for _Session class.'); + Parse.Cloud.beforeFind('_Session', () => {}); + }).toThrow('beforeFind/afterFind is not allowed on _Session class.'); expect(() => { - Parse.Cloud.afterSave('_Session', () => {}); - }).toThrow('Triggers are not supported for _Session class.'); + Parse.Cloud.afterFind('_Session', () => {}); + }).toThrow('beforeFind/afterFind is not allowed on _Session class.'); expect(() => { Parse.Cloud.beforeSave('_PushStatus', () => {}); }).toThrow('Only afterSave is allowed on _PushStatus'); diff --git a/spec/CloudCodeSessionTrigger.spec.js b/spec/CloudCodeSessionTrigger.spec.js new file mode 100644 index 0000000000..bf9b2a8ddf --- /dev/null +++ b/spec/CloudCodeSessionTrigger.spec.js @@ -0,0 +1,171 @@ +'use strict'; +const Parse = require('parse/node'); + +describe('CloudCode _Session Trigger tests', () => { + describe('beforeSave', () => { + it('should run normally', async () => { + Parse.Cloud.beforeSave('_Session', async req => { + const sessionObject = req.object; + expect(sessionObject).toBeDefined(); + expect(sessionObject.get('sessionToken')).toBeDefined(); + expect(sessionObject.get('createdWith')).toBeDefined(); + expect(sessionObject.get('user')).toBeDefined(); + expect(sessionObject.get('user')).toEqual(jasmine.any(Parse.User)); + }); + // signUp a user (internally creates a session) + const user = new Parse.User(); + user.setUsername('some-user-name'); + user.setPassword('password'); + await user.signUp(); + }); + + it('should discard any changes', async () => { + Parse.Cloud.beforeSave('_Session', function (req) { + // perform some changes + req.object.set('KeyA', 'EDITED_VALUE'); + req.object.set('KeyB', 'EDITED_VALUE'); + }); + // signUp a user (internally creates a session) + const user = new Parse.User(); + user.setUsername('some-user-name'); + user.setPassword('password'); + await user.signUp(); + // get the session + const query = new Parse.Query('_Session'); + query.equalTo('user', user); + const sessionObject = await query.first({ + useMasterKey: true, + }); + // expect un-edited object + expect(sessionObject.get('KeyA')).toBeUndefined(); + expect(sessionObject.get('KeyB')).toBeUndefined(); + expect(sessionObject.get('user')).toBeDefined(); + expect(sessionObject.get('user').id).toBe(user.id); + expect(sessionObject.get('sessionToken')).toBeDefined(); + }); + + it('should follow user creation flow during signUp without being affected by errors', async () => { + Parse.Cloud.beforeSave('_Session', async () => { + // reject the session + throw new Parse.Error(12345678, 'Sorry, more steps are required'); + }); + Parse.Cloud.beforeSave('_User', async req => { + // make sure this runs correctly + req.object.set('firstName', 'abcd'); + }); + Parse.Cloud.afterSave('_User', async req => { + if (req.object.has('lastName')) { + return; + } + // make sure this runs correctly + req.object.set('lastName', '1234'); + await req.object.save({}, { + useMasterKey: true + }); + }); + + const user = new Parse.User(); + user.setUsername('user-name'); + user.setPassword('user-password'); + await user.signUp(); + + expect(user.getSessionToken()).toBeUndefined(); + await delay(200); // just so that afterSave has time to run + await user.fetch({ + useMasterKey: true + }); + expect(user.get('username')).toBe('user-name'); + expect(user.get('firstName')).toBe('abcd'); + expect(user.get('lastName')).toBe('1234'); + // get the session + const query2 = new Parse.Query('_Session'); + query2.equalTo('user', user); + const sessionObject = await query2.first({ + useMasterKey: true, + }); + expect(sessionObject).toBeUndefined(); + }); + + it('should fail and prevent login on throw', async () => { + Parse.Cloud.beforeSave('_Session', async req => { + const sessionObject = req.object; + if (sessionObject.get('createdWith').action === 'login') { + throw new Parse.Error(12345678, 'Sorry, you cant login :('); + } + }); + const user = new Parse.User(); + user.setUsername('some-username'); + user.setPassword('password'); + await user.signUp(); + await Parse.User.logOut(); + try { + await Parse.User.logIn('some-username', 'password'); + throw 'Log in should have failed'; + } catch (error) { + expect(error.code).toBe(12345678); + expect(error.message).toBe('Sorry, you cant login :('); + } + // make sure no session was created + const query = new Parse.Query('_Session'); + query.equalTo('user', user); + const sessionObject = await query.first({ + useMasterKey: true, + }); + expect(sessionObject).toBeUndefined(); + }); + }); + + describe('beforeDelete', () => { + it('should ignore thrown errors', async () => { + Parse.Cloud.beforeDelete('_Session', async () => { + throw new Parse.Error(12345678, 'Nop'); + }); + const user = new Parse.User(); + user.setUsername('some-user-name'); + user.setPassword('password'); + await user.signUp(); + await user.destroy({ + useMasterKey: true + }); + try { + await user.fetch({ + useMasterKey: true + }); + throw 'User should have been deleted.'; + } catch (error) { + expect(error.code).toBe(Parse.Error.OBJECT_NOT_FOUND); + } + }); + }); + + describe('afterDelete', () => { + it('should work normally', async () => { + let callCount = 0; + Parse.Cloud.afterDelete('_Session', function () { + callCount++; + }); + const user = new Parse.User(); + user.setUsername('some-user-name'); + user.setPassword('password'); + await user.signUp(); + await Parse.User.logOut(); + await delay(200); + expect(callCount).toEqual(1) + }); + }); + + describe('afterSave', () => { + it('should work normally', async () => { + let callCount = 0 + Parse.Cloud.afterSave('_Session', function () { + callCount++; + }); + const user = new Parse.User(); + user.setUsername('some-user-name'); + user.setPassword('password'); + await user.signUp(); + await delay(200); + expect(callCount).toEqual(1) + }); + }); +}); diff --git a/spec/Parse.Push.spec.js b/spec/Parse.Push.spec.js index 6a424935b9..8490b494b2 100644 --- a/spec/Parse.Push.spec.js +++ b/spec/Parse.Push.spec.js @@ -2,18 +2,12 @@ const request = require('../lib/request'); -const delayPromise = delay => { - return new Promise(resolve => { - setTimeout(resolve, delay); - }); -}; - describe('Parse.Push', () => { - const setup = function() { + const setup = function () { const sendToInstallationSpy = jasmine.createSpy(); const pushAdapter = { - send: function(body, installations) { + send: function (body, installations) { const badge = body.data.badge; const promises = installations.map(installation => { sendToInstallationSpy(installation); @@ -32,7 +26,7 @@ describe('Parse.Push', () => { }); return Promise.all(promises); }, - getValidPushTypes: function() { + getValidPushTypes: function () { return ['ios', 'android']; }, }; @@ -78,21 +72,22 @@ describe('Parse.Push', () => { it('should properly send push', done => { return setup() - .then(({ sendToInstallationSpy }) => { - return Parse.Push.send( - { - where: { - deviceType: 'ios', - }, - data: { - badge: 'Increment', - alert: 'Hello world!', - }, + .then(({ + sendToInstallationSpy + }) => { + return Parse.Push.send({ + where: { + deviceType: 'ios', }, - { useMasterKey: true } - ) + data: { + badge: 'Increment', + alert: 'Hello world!', + }, + }, { + useMasterKey: true + }) .then(() => { - return delayPromise(500); + return delay(500); }) .then(() => { expect(sendToInstallationSpy.calls.count()).toEqual(10); @@ -110,21 +105,20 @@ describe('Parse.Push', () => { it('should properly send push with lowercaseIncrement', done => { return setup() .then(() => { - return Parse.Push.send( - { - where: { - deviceType: 'ios', - }, - data: { - badge: 'increment', - alert: 'Hello world!', - }, + return Parse.Push.send({ + where: { + deviceType: 'ios', + }, + data: { + badge: 'increment', + alert: 'Hello world!', }, - { useMasterKey: true } - ); + }, { + useMasterKey: true + }); }) .then(() => { - return delayPromise(500); + return delay(500); }) .then(() => { done(); @@ -138,20 +132,19 @@ describe('Parse.Push', () => { it('should not allow clients to query _PushStatus', done => { setup() .then(() => - Parse.Push.send( - { - where: { - deviceType: 'ios', - }, - data: { - badge: 'increment', - alert: 'Hello world!', - }, + Parse.Push.send({ + where: { + deviceType: 'ios', + }, + data: { + badge: 'increment', + alert: 'Hello world!', }, - { useMasterKey: true } - ) + }, { + useMasterKey: true + }) ) - .then(() => delayPromise(500)) + .then(() => delay(500)) .then(() => { request({ url: 'http://localhost:8378/1/classes/_PushStatus', @@ -173,20 +166,19 @@ describe('Parse.Push', () => { it('should allow master key to query _PushStatus', done => { setup() .then(() => - Parse.Push.send( - { - where: { - deviceType: 'ios', - }, - data: { - badge: 'increment', - alert: 'Hello world!', - }, + Parse.Push.send({ + where: { + deviceType: 'ios', + }, + data: { + badge: 'increment', + alert: 'Hello world!', }, - { useMasterKey: true } - ) + }, { + useMasterKey: true + }) ) - .then(() => delayPromise(500)) // put a delay as we keep writing + .then(() => delay(500)) // put a delay as we keep writing .then(() => { request({ url: 'http://localhost:8378/1/classes/_PushStatus', @@ -216,20 +208,21 @@ describe('Parse.Push', () => { }); it('should throw error if missing push configuration', done => { - reconfigureServer({ push: null }) + reconfigureServer({ + push: null + }) .then(() => { - return Parse.Push.send( - { - where: { - deviceType: 'ios', - }, - data: { - badge: 'increment', - alert: 'Hello world!', - }, + return Parse.Push.send({ + where: { + deviceType: 'ios', }, - { useMasterKey: true } - ); + data: { + badge: 'increment', + alert: 'Hello world!', + }, + }, { + useMasterKey: true + }); }) .then( () => { @@ -246,7 +239,7 @@ describe('Parse.Push', () => { }); }); - const successfulAny = function(body, installations) { + const successfulAny = function (body, installations) { const promises = installations.map(device => { return Promise.resolve({ transmitted: true, @@ -257,7 +250,7 @@ describe('Parse.Push', () => { return Promise.all(promises); }; - const provideInstallations = function(num) { + const provideInstallations = function (num) { if (!num) { num = 2; } @@ -279,14 +272,14 @@ describe('Parse.Push', () => { }; const losingAdapter = { - send: function(body, installations) { + send: function (body, installations) { // simulate having lost an installation before this was called // thus invalidating our 'count' in _PushStatus installations.pop(); return successfulAny(body, installations); }, - getValidPushTypes: function() { + getValidPushTypes: function () { return ['android']; }, }; @@ -298,19 +291,24 @@ describe('Parse.Push', () => { */ it("does not get stuck with _PushStatus 'running' on 1 installation lost", done => { reconfigureServer({ - push: { adapter: losingAdapter }, + push: { + adapter: losingAdapter + }, }) .then(() => { return Parse.Object.saveAll(provideInstallations()); }) .then(() => { - return Parse.Push.send( - { - data: { alert: 'We fixed our status!' }, - where: { deviceType: 'android' }, + return Parse.Push.send({ + data: { + alert: 'We fixed our status!' + }, + where: { + deviceType: 'android' }, - { useMasterKey: true } - ); + }, { + useMasterKey: true + }); }) .then(() => { // it is enqueued so it can take time @@ -323,7 +321,9 @@ describe('Parse.Push', () => { .then(() => { // query for push status const query = new Parse.Query('_PushStatus'); - return query.find({ useMasterKey: true }); + return query.find({ + useMasterKey: true + }); }) .then(results => { // verify status is NOT broken @@ -357,14 +357,14 @@ describe('Parse.Push', () => { reconfigureServer({ push: { adapter: { - send: function(body, installations) { + send: function (body, installations) { // simulate having added an installation before this was called // thus invalidating our 'count' in _PushStatus installations.push(iOSInstallation); return successfulAny(body, installations); }, - getValidPushTypes: function() { + getValidPushTypes: function () { return ['android']; }, }, @@ -374,13 +374,18 @@ describe('Parse.Push', () => { return Parse.Object.saveAll(installations); }) .then(() => { - return Parse.Push.send( - { - data: { alert: 'We fixed our status!' }, - where: { deviceType: { $ne: 'random' } }, + return Parse.Push.send({ + data: { + alert: 'We fixed our status!' }, - { useMasterKey: true } - ); + where: { + deviceType: { + $ne: 'random' + } + }, + }, { + useMasterKey: true + }); }) .then(() => { // it is enqueued so it can take time @@ -393,7 +398,9 @@ describe('Parse.Push', () => { .then(() => { // query for push status const query = new Parse.Query('_PushStatus'); - return query.find({ useMasterKey: true }); + return query.find({ + useMasterKey: true + }); }) .then(results => { // verify status is NOT broken @@ -416,19 +423,24 @@ describe('Parse.Push', () => { const installations = provideInstallations(devices); reconfigureServer({ - push: { adapter: losingAdapter }, + push: { + adapter: losingAdapter + }, }) .then(() => { return Parse.Object.saveAll(installations); }) .then(() => { - return Parse.Push.send( - { - data: { alert: 'We fixed our status!' }, - where: { deviceType: 'android' }, + return Parse.Push.send({ + data: { + alert: 'We fixed our status!' }, - { useMasterKey: true } - ); + where: { + deviceType: 'android' + }, + }, { + useMasterKey: true + }); }) .then(() => { // it is enqueued so it can take time @@ -441,7 +453,9 @@ describe('Parse.Push', () => { .then(() => { // query for push status const query = new Parse.Query('_PushStatus'); - return query.find({ useMasterKey: true }); + return query.find({ + useMasterKey: true + }); }) .then(results => { // verify status is NOT broken @@ -485,14 +499,14 @@ describe('Parse.Push', () => { reconfigureServer({ push: { adapter: { - send: function(body, installations) { + send: function (body, installations) { // simulate having added an installation before this was called // thus invalidating our 'count' in _PushStatus installations.push(iOSInstallations.pop()); return successfulAny(body, installations); }, - getValidPushTypes: function() { + getValidPushTypes: function () { return ['android']; }, }, @@ -502,13 +516,18 @@ describe('Parse.Push', () => { return Parse.Object.saveAll(installations); }) .then(() => { - return Parse.Push.send( - { - data: { alert: 'We fixed our status!' }, - where: { deviceType: { $ne: 'random' } }, + return Parse.Push.send({ + data: { + alert: 'We fixed our status!' }, - { useMasterKey: true } - ); + where: { + deviceType: { + $ne: 'random' + } + }, + }, { + useMasterKey: true + }); }) .then(() => { // it is enqueued so it can take time @@ -521,7 +540,9 @@ describe('Parse.Push', () => { .then(() => { // query for push status const query = new Parse.Query('_PushStatus'); - return query.find({ useMasterKey: true }); + return query.find({ + useMasterKey: true + }); }) .then(results => { // verify status is NOT broken diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index b3666482c3..dbe5a611cc 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -13,13 +13,8 @@ let database; const defaultColumns = require('../lib/Controllers/SchemaController') .defaultColumns; -const delay = function delay(delay) { - return new Promise(resolve => setTimeout(resolve, delay)); -}; - const installationSchema = { - fields: Object.assign( - {}, + fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation ), @@ -210,7 +205,9 @@ describe('Installations', () => { .create(config, auth.nobody(config), '_Installation', input) .then(() => { const query = new Parse.Query(Parse.Installation); - return query.find({ useMasterKey: true }); + return query.find({ + useMasterKey: true + }); }) .then(results => { expect(results.length).toEqual(1); @@ -416,28 +413,28 @@ describe('Installations', () => { }) .then(() => database.adapter.find( - '_Installation', - { installationId: installId1 }, - installationSchema, - {} + '_Installation', { + installationId: installId1 + }, + installationSchema, {} ) ) .then(results => { expect(results.length).toEqual(1); return database.adapter.find( - '_Installation', - { installationId: installId2 }, - installationSchema, - {} + '_Installation', { + installationId: installId2 + }, + installationSchema, {} ); }) .then(results => { expect(results.length).toEqual(1); return database.adapter.find( - '_Installation', - { installationId: installId3 }, - installationSchema, - {} + '_Installation', { + installationId: installId3 + }, + installationSchema, {} ); }) .then(results => { @@ -469,8 +466,9 @@ describe('Installations', () => { return rest.update( config, auth.nobody(config), - '_Installation', - { objectId }, + '_Installation', { + objectId + }, update ); }) @@ -504,12 +502,15 @@ describe('Installations', () => { ) .then(results => { expect(results.length).toEqual(1); - input = { installationId: installId2 }; + input = { + installationId: installId2 + }; return rest.update( config, auth.nobody(config), - '_Installation', - { objectId: results[0].objectId }, + '_Installation', { + objectId: results[0].objectId + }, input ); }) @@ -540,12 +541,15 @@ describe('Installations', () => { ) .then(results => { expect(results.length).toEqual(1); - input = { deviceToken: b }; + input = { + deviceToken: b + }; return rest.update( config, auth.nobody(config), - '_Installation', - { objectId: results[0].objectId }, + '_Installation', { + objectId: results[0].objectId + }, input ); }) @@ -585,8 +589,9 @@ describe('Installations', () => { return rest.update( config, auth.nobody(config), - '_Installation', - { objectId: results[0].objectId }, + '_Installation', { + objectId: results[0].objectId + }, input ); }) @@ -624,8 +629,9 @@ describe('Installations', () => { return rest.update( config, auth.nobody(config), - '_Installation', - { objectId: results[0].objectId }, + '_Installation', { + objectId: results[0].objectId + }, input ); }) @@ -659,8 +665,9 @@ describe('Installations', () => { return rest.update( config, auth.nobody(config), - '_Installation', - { objectId: results[0].objectId }, + '_Installation', { + objectId: results[0].objectId + }, input ); }) @@ -698,9 +705,9 @@ describe('Installations', () => { .then(() => database.adapter.find( '_Installation', - installationSchema, - { installationId: installId1 }, - {} + installationSchema, { + installationId: installId1 + }, {} ) ) .then(results => { @@ -708,9 +715,9 @@ describe('Installations', () => { expect(results.length).toEqual(1); return database.adapter.find( '_Installation', - installationSchema, - { installationId: installId2 }, - {} + installationSchema, { + installationId: installId2 + }, {} ); }) .then(results => { @@ -724,17 +731,18 @@ describe('Installations', () => { return rest.update( config, auth.nobody(config), - '_Installation', - { objectId: secondObject.objectId }, + '_Installation', { + objectId: secondObject.objectId + }, input ); }) .then(() => database.adapter.find( '_Installation', - installationSchema, - { objectId: firstObject.objectId }, - {} + installationSchema, { + objectId: firstObject.objectId + }, {} ) ) .then(results => { @@ -773,9 +781,9 @@ describe('Installations', () => { .then(() => database.adapter.find( '_Installation', - installationSchema, - { installationId: installId1 }, - {} + installationSchema, { + installationId: installId1 + }, {} ) ) .then(results => { @@ -786,9 +794,9 @@ describe('Installations', () => { .then(() => database.adapter.find( '_Installation', - installationSchema, - { installationId: installId2 }, - {} + installationSchema, { + installationId: installId2 + }, {} ) ) .then(results => { @@ -802,8 +810,9 @@ describe('Installations', () => { return rest.update( config, auth.nobody(config), - '_Installation', - { objectId: secondObject.objectId }, + '_Installation', { + objectId: secondObject.objectId + }, input ); }) @@ -811,9 +820,9 @@ describe('Installations', () => { .then(() => database.adapter.find( '_Installation', - installationSchema, - { objectId: firstObject.objectId }, - {} + installationSchema, { + objectId: firstObject.objectId + }, {} ) ) .then(results => { @@ -882,8 +891,9 @@ describe('Installations', () => { return rest.update( config, auth.nobody(config), - '_Installation', - { objectId: results[0].objectId }, + '_Installation', { + objectId: results[0].objectId + }, input ); }) @@ -923,9 +933,9 @@ describe('Installations', () => { .then(() => database.adapter.find( '_Installation', - installationSchema, - { deviceToken: t }, - {} + installationSchema, { + deviceToken: t + }, {} ) ) .then(results => { @@ -938,8 +948,9 @@ describe('Installations', () => { return rest.update( config, auth.nobody(config), - '_Installation', - { objectId: results[0].objectId }, + '_Installation', { + objectId: results[0].objectId + }, input ); }) @@ -979,9 +990,9 @@ describe('Installations', () => { .then(() => database.adapter.find( '_Installation', - installationSchema, - { deviceToken: t }, - {} + installationSchema, { + deviceToken: t + }, {} ) ) .then(results => { @@ -998,8 +1009,9 @@ describe('Installations', () => { return rest.update( config, auth.nobody(config), - '_Installation', - { objectId: results[0].objectId }, + '_Installation', { + objectId: results[0].objectId + }, input ); }) @@ -1047,9 +1059,9 @@ describe('Installations', () => { .then(() => database.adapter.find( '_Installation', - installationSchema, - { deviceToken: t }, - {} + installationSchema, { + deviceToken: t + }, {} ) ) .then(results => { @@ -1063,17 +1075,18 @@ describe('Installations', () => { return rest.update( config, auth.nobody(config), - '_Installation', - { objectId: installObj.objectId }, + '_Installation', { + objectId: installObj.objectId + }, input ); }) .then(() => database.adapter.find( '_Installation', - installationSchema, - { objectId: tokenObj.objectId }, - {} + installationSchema, { + objectId: tokenObj.objectId + }, {} ) ) .then(results => { @@ -1115,9 +1128,9 @@ describe('Installations', () => { .then(() => database.adapter.find( '_Installation', - installationSchema, - { deviceToken: t }, - {} + installationSchema, { + deviceToken: t + }, {} ) ) .then(results => { @@ -1135,17 +1148,18 @@ describe('Installations', () => { return rest.update( config, auth.nobody(config), - '_Installation', - { objectId: installObj.objectId }, + '_Installation', { + objectId: installObj.objectId + }, input ); }) .then(() => database.adapter.find( '_Installation', - installationSchema, - { objectId: tokenObj.objectId }, - {} + installationSchema, { + objectId: tokenObj.objectId + }, {} ) ) .then(results => { @@ -1225,8 +1239,7 @@ describe('Installations', () => { }; return request({ headers: headers, - url: - 'http://localhost:8378/1/installations/' + + url: 'http://localhost:8378/1/installations/' + createResult.response.objectId, }).then(response => { const body = response.data; @@ -1292,7 +1305,9 @@ describe('Installations', () => { createResult.response.objectId ); installationObj.set('customField', 'custom value'); - return installationObj.save(null, { useMasterKey: true }); + return installationObj.save(null, { + useMasterKey: true + }); }) .then(updateResult => { expect(updateResult).not.toBeUndefined(); @@ -1319,14 +1334,15 @@ describe('Installations', () => { const query = new Parse.Query(Parse.Installation); query.equalTo('installationId', installId); query - .first({ useMasterKey: true }) + .first({ + useMasterKey: true + }) .then(installation => { - return installation.save( - { - key: 'value', - }, - { useMasterKey: true } - ); + return installation.save({ + key: 'value', + }, { + useMasterKey: true + }); }) .then( () => { @@ -1353,15 +1369,16 @@ describe('Installations', () => { const query = new Parse.Query(Parse.Installation); query.equalTo('installationId', installId); query - .first({ useMasterKey: true }) + .first({ + useMasterKey: true + }) .then(installation => { - return installation.save( - { - key: 'value', - installationId: '22222222-abcd-abcd-abcd-123456789abc', - }, - { useMasterKey: true } - ); + return installation.save({ + key: 'value', + installationId: '22222222-abcd-abcd-abcd-123456789abc', + }, { + useMasterKey: true + }); }) .then( () => { diff --git a/spec/helper.js b/spec/helper.js index 607f2fcf1d..c5924b21b7 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -8,8 +8,12 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = jasmine.getEnv().clearReporters(); jasmine.getEnv().addReporter( new SpecReporter({ - colors: { enabled: supportsColor.stdout }, - spec: { displayDuration: true }, + colors: { + enabled: supportsColor.stdout, + }, + spec: { + displayDuration: true, + }, }) ); @@ -299,12 +303,15 @@ function createTestUser() { function ok(bool, message) { expect(bool).toBeTruthy(message); } + function equal(a, b, message) { expect(a).toEqual(b, message); } + function strictEqual(a, b, message) { expect(a).toBe(b, message); } + function notEqual(a, b, message) { expect(a).not.toEqual(b, message); } @@ -387,6 +394,11 @@ function mockShortLivedAuth() { return auth; } +// delay +function delay(millis) { + return new Promise(r => setTimeout(r, millis)); +} + // This is polluting, but, it makes it way easier to directly port old tests. global.Parse = Parse; global.TestObject = TestObject; @@ -404,6 +416,7 @@ global.range = range; global.reconfigureServer = reconfigureServer; global.defaultConfiguration = defaultConfiguration; global.mockFacebookAuthenticator = mockFacebookAuthenticator; +global.delay = delay; global.jfail = function(err) { fail(JSON.stringify(err)); }; diff --git a/src/RestWrite.js b/src/RestWrite.js index 315402cb00..81a1da01bc 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -741,11 +741,23 @@ RestWrite.prototype.createSessionToken = function() { installationId: this.auth.installationId, }); - if (this.response && this.response.response) { - this.response.response.sessionToken = sessionData.sessionToken; - } - - return createSession(); + return createSession() + .then(() => { + // session was created, populate the sessionToken + if (this.response && this.response.response) { + this.response.response.sessionToken = sessionData.sessionToken; + } + }) + .catch(e => { + // we need to swallow the error during a Signup + // this is to make sure the .execute() chain continues normally for the user. + // we dont need to kwow about any errors when signing a user up. + if (!this.storage['authProvider']) { + return Promise.resolve(); + } + // on login proppagate errors + throw e; + }); }; // Delete email reset tokens if user is changing password or email. @@ -1482,7 +1494,7 @@ RestWrite.prototype.objectId = function() { }; // Returns a copy of the data and delete bad keys (_auth_data, _hashed_password...) -RestWrite.prototype.sanitizedData = function() { +RestWrite.prototype.sanitizedData = function(decodeData = false) { const data = Object.keys(this.data).reduce((data, key) => { // Regexp comes from Parse.Object.prototype.validate if (!/^[A-Za-z][0-9A-Za-z_]*$/.test(key)) { @@ -1490,11 +1502,26 @@ RestWrite.prototype.sanitizedData = function() { } return data; }, deepcopy(this.data)); - return Parse._decode(undefined, data); + if (decodeData) { + // decoded data might contain instances of ParseObject, ParseRelation, ParseACl... + return Parse._decode(undefined, data); + } + // data is kept in json format. Not decoded + return data; }; // Returns an updated copy of the object RestWrite.prototype.buildUpdatedObject = function(extraData) { + if (this.className === '_Session') { + // on Session, 'updatedObject' will be an instance of 'ParseSession'. + // 'ParseSession' prevents setting readOnlyKeys and in turn '.set' fails. + // So, 'beforeSave' on session will show the full object in req.object with + // req.object.dirtyKeys being empty []. + // This is okay, since sessions are mostly never updated, only created or destroyed. + // Additionally sanitizedData should be kept in json, not decoded. + // because '.inflate' internally uses '.fromJSON' so it expects data to be JSON to work properly. + return triggers.inflate(extraData, this.sanitizedData(false)); + } const updatedObject = triggers.inflate(extraData, this.originalData); Object.keys(this.data).reduce(function(data, key) { if (key.indexOf('.') > 0) { @@ -1511,8 +1538,7 @@ RestWrite.prototype.buildUpdatedObject = function(extraData) { } return data; }, deepcopy(this.data)); - - updatedObject.set(this.sanitizedData()); + updatedObject.set(this.sanitizedData(true)); return updatedObject; }; diff --git a/src/triggers.js b/src/triggers.js index ead83cac5c..d45c4ad87e 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1,6 +1,8 @@ // triggers.js import Parse from 'parse/node'; -import { logger } from './logger'; +import { + logger +} from './logger'; export const Types = { beforeSave: 'beforeSave', @@ -11,12 +13,12 @@ export const Types = { afterFind: 'afterFind', }; -const baseStore = function() { +const baseStore = function () { const Validators = {}; const Functions = {}; const Jobs = {}; const LiveQuery = []; - const Triggers = Object.keys(Types).reduce(function(base, key) { + const Triggers = Object.keys(Types).reduce(function (base, key) { base[key] = {}; return base; }, {}); @@ -31,16 +33,18 @@ const baseStore = function() { }; function validateClassNameForTriggers(className, type) { - const restrictedClassNames = ['_Session']; - if (restrictedClassNames.indexOf(className) != -1) { - throw `Triggers are not supported for ${className} class.`; - } if (type == Types.beforeSave && className === '_PushStatus') { // _PushStatus uses undocumented nested key increment ops // allowing beforeSave would mess up the objects big time // TODO: Allow proper documented way of using nested increment ops throw 'Only afterSave is allowed on _PushStatus'; } + if ( + (type == Types.beforeFind || type == Types.afterFind) && + className === '_Session' + ) { + throw 'beforeFind/afterFind is not allowed on _Session class.'; + } return className; } @@ -240,8 +244,10 @@ export function getRequestQueryObject( // transform them to Parse.Object instances expected by Cloud Code. // Any changes made to the object in a beforeSave will be included. export function getResponseObject(request, resolve, reject) { + const className = request.object ? request.object.className : null; + const isSessionTrigger = className === '_Session'; return { - success: function(response) { + success: function (response) { if (request.triggerName === Types.afterFind) { if (!response) { response = request.objects; @@ -251,6 +257,10 @@ export function getResponseObject(request, resolve, reject) { }); return resolve(response); } + // ignore edited object in session triggers + if (isSessionTrigger) { + return resolve(); + } // Use the JSON response if ( response && @@ -265,7 +275,11 @@ export function getResponseObject(request, resolve, reject) { } return resolve(response); }, - error: function(error) { + error: function (error) { + // ignore errors thrown in before-delete (during logout for example) + if (isSessionTrigger && request.triggerName === Types.beforeDelete) { + return resolve(undefined, error); + } if (error instanceof Parse.Error) { reject(error); } else if (error instanceof Error) { @@ -281,13 +295,13 @@ function userIdForLog(auth) { return auth && auth.user ? auth.user.id : undefined; } + function logTriggerAfterHook(triggerType, className, input, auth) { const cleanInput = logger.truncateLogMessage(JSON.stringify(input)); logger.info( `${triggerType} triggered for ${className} for user ${userIdForLog( auth - )}:\n Input: ${cleanInput}`, - { + )}:\n Input: ${cleanInput}`, { className, triggerType, user: userIdForLog(auth), @@ -307,8 +321,7 @@ function logTriggerSuccessBeforeHook( logger.info( `${triggerType} triggered for ${className} for user ${userIdForLog( auth - )}:\n Input: ${cleanInput}\n Result: ${cleanResult}`, - { + )}:\n Input: ${cleanInput}\n Result: ${cleanResult}`, { className, triggerType, user: userIdForLog(auth), @@ -321,8 +334,20 @@ function logTriggerErrorBeforeHook(triggerType, className, input, auth, error) { logger.error( `${triggerType} failed for ${className} for user ${userIdForLog( auth - )}:\n Input: ${cleanInput}\n Error: ${JSON.stringify(error)}`, - { + )}:\n Input: ${cleanInput}\n Error: ${JSON.stringify(error)}`, { + className, + triggerType, + error, + user: userIdForLog(auth), + } + ); +} + +function logWarningWhenErrorIsIgnored(triggerType, className, auth, error) { + logger.warn( + `Error ignored in ${triggerType} for ${className} for user ${userIdForLog( + auth + )}:\n Error: ${JSON.stringify(error)}`, { className, triggerType, error, @@ -344,7 +369,10 @@ export function maybeRunAfterFindTrigger( return resolve(); } const request = getRequestObject(triggerType, auth, null, null, config); - const { success, error } = getResponseObject( + const { + success, + error + } = getResponseObject( request, object => { resolve(object); @@ -509,7 +537,7 @@ export function maybeRunTrigger( if (!parseObject) { return Promise.resolve({}); } - return new Promise(function(resolve, reject) { + return new Promise(function (resolve, reject) { var trigger = getTrigger( parseObject.className, triggerType, @@ -524,9 +552,15 @@ export function maybeRunTrigger( config, context ); - var { success, error } = getResponseObject( + const { + success, + error + } = getResponseObject( request, - object => { + (object, error) => { + if (error) { + logWarningWhenErrorIsIgnored(triggerType, parseObject.className, auth, error); + } logTriggerSuccessBeforeHook( triggerType, parseObject.className, @@ -582,7 +616,9 @@ export function maybeRunTrigger( // Converts a REST-format object to a Parse.Object // data is either className or an object export function inflate(data, restObject) { - var copy = typeof data == 'object' ? data : { className: data }; + const copy = typeof data == 'object' ? data : { + className: data + }; for (var key in restObject) { copy[key] = restObject[key]; }