diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index 261733c3af..cbf8e31f10 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -544,6 +544,110 @@ describe('Installations', () => { }); }); + it('clears deviceToken via Delete op without crashing the lookup', done => { + const installId = '12345678-abcd-abcd-abcd-123456789abc'; + const t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; + const input = { + installationId: installId, + deviceType: 'ios', + deviceToken: t, + }; + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(results => { + expect(results.length).toEqual(1); + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: results[0].objectId }, + { deviceToken: { __op: 'Delete' } } + ); + }) + .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(results => { + expect(results.length).toEqual(1); + expect(results[0].deviceToken).toBeUndefined(); + expect(results[0].installationId).toEqual(installId); + done(); + }) + .catch(err => { + jfail(err); + done(); + }); + }); + + it('clears deviceToken via null without crashing the lookup', done => { + const installId = '12345678-abcd-abcd-abcd-123456789abc'; + const t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; + const input = { + installationId: installId, + deviceType: 'ios', + deviceToken: t, + }; + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(results => { + expect(results.length).toEqual(1); + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: results[0].objectId }, + { deviceToken: null } + ); + }) + .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(results => { + expect(results.length).toEqual(1); + expect(results[0].deviceToken == null).toBeTrue(); + expect(results[0].installationId).toEqual(installId); + done(); + }) + .catch(err => { + jfail(err); + done(); + }); + }); + + it('clears deviceToken alongside another field update', done => { + const installId = '12345678-abcd-abcd-abcd-123456789abc'; + const t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; + const input = { + installationId: installId, + deviceType: 'ios', + deviceToken: t, + appVersion: '1', + }; + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(results => { + expect(results.length).toEqual(1); + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: results[0].objectId }, + { deviceToken: { __op: 'Delete' }, deviceType: 'ios', appVersion: '2' } + ); + }) + .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(results => { + expect(results.length).toEqual(1); + expect(results[0].deviceToken).toBeUndefined(); + expect(results[0].appVersion).toEqual('2'); + expect(results[0].installationId).toEqual(installId); + done(); + }) + .catch(err => { + jfail(err); + done(); + }); + }); + it('update fails to change deviceType', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; let input = { diff --git a/src/RestWrite.js b/src/RestWrite.js index 6d3c0d35a9..f1a0b73cb7 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1295,9 +1295,19 @@ RestWrite.prototype.handleInstallation = function () { return; } + // A client clearing the deviceToken sends either null or { __op: 'Delete' }. + // Treat that as "no deviceToken to identify/match by" so we do not feed the + // operator object into the lookup query (which would fail Mongo transform). + const clearingDeviceToken = + this.data.deviceToken === null || + (typeof this.data.deviceToken === 'object' && + this.data.deviceToken !== null && + this.data.deviceToken.__op === 'Delete'); + let deviceTokenForLookup = clearingDeviceToken ? undefined : this.data.deviceToken; + if ( !this.query && - !this.data.deviceToken && + !deviceTokenForLookup && !this.data.installationId && !this.auth.installationId ) { @@ -1309,8 +1319,9 @@ RestWrite.prototype.handleInstallation = function () { // If the device token is 64 characters long, we assume it is for iOS // and lowercase it. - if (this.data.deviceToken && this.data.deviceToken.length == 64) { - this.data.deviceToken = this.data.deviceToken.toLowerCase(); + if (deviceTokenForLookup && deviceTokenForLookup.length == 64) { + this.data.deviceToken = deviceTokenForLookup.toLowerCase(); + deviceTokenForLookup = this.data.deviceToken; } // We lowercase the installationId if present @@ -1330,7 +1341,7 @@ RestWrite.prototype.handleInstallation = function () { } // Updating _Installation but not updating anything critical - if (this.query && !this.data.deviceToken && !installationId && !this.data.deviceType) { + if (this.query && !deviceTokenForLookup && !installationId && !this.data.deviceType) { return; } @@ -1353,8 +1364,8 @@ RestWrite.prototype.handleInstallation = function () { installationId: installationId, }); } - if (this.data.deviceToken) { - orQueries.push({ deviceToken: this.data.deviceToken }); + if (deviceTokenForLookup) { + orQueries.push({ deviceToken: deviceTokenForLookup }); } if (orQueries.length == 0) { @@ -1379,7 +1390,7 @@ RestWrite.prototype.handleInstallation = function () { if (result.installationId == installationId) { installationIdMatch = result; } - if (result.deviceToken == this.data.deviceToken) { + if (deviceTokenForLookup && result.deviceToken == deviceTokenForLookup) { deviceTokenMatches.push(result); } }); @@ -1397,9 +1408,9 @@ RestWrite.prototype.handleInstallation = function () { throw new Parse.Error(136, 'installationId may not be changed in this ' + 'operation'); } if ( - this.data.deviceToken && + deviceTokenForLookup && objectIdMatch.deviceToken && - this.data.deviceToken !== objectIdMatch.deviceToken && + deviceTokenForLookup !== objectIdMatch.deviceToken && !this.data.installationId && !objectIdMatch.installationId ) { @@ -1451,7 +1462,7 @@ RestWrite.prototype.handleInstallation = function () { // deviceToken, and return nil to signal that a new object should be // created. const delQuery = { - deviceToken: this.data.deviceToken, + deviceToken: deviceTokenForLookup, installationId: { $ne: installationId, }, @@ -1486,12 +1497,12 @@ RestWrite.prototype.handleInstallation = function () { validSchemaController: this.validSchemaController, }); } else { - if (this.data.deviceToken && idMatch.deviceToken != this.data.deviceToken) { + if (deviceTokenForLookup && idMatch.deviceToken != deviceTokenForLookup) { // We're setting the device token on an existing installation, so // we should try cleaning out old installations that match this // device token. const delQuery = { - deviceToken: this.data.deviceToken, + deviceToken: deviceTokenForLookup, }; // We have a unique install Id, use that to preserve // the interesting installation