diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index 261733c3af..99bbbbcb27 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -544,6 +544,127 @@ describe('Installations', () => { }); }); + it('update fails to clear installationId via Delete op', done => { + const installId = '12345678-abcd-abcd-abcd-123456789abc'; + const input = { + installationId: installId, + deviceType: 'ios', + }; + 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 }, + { installationId: { __op: 'Delete' } } + ); + }) + .then(() => { + fail('Updating the installation should have failed.'); + done(); + }) + .catch(error => { + expect(error.code).toEqual(136); + done(); + }); + }); + + it('update fails to clear installationId via null', done => { + const installId = '12345678-abcd-abcd-abcd-123456789abc'; + const input = { + installationId: installId, + deviceType: 'ios', + }; + 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 }, + { installationId: null } + ); + }) + .then(() => { + fail('Updating the installation should have failed.'); + done(); + }) + .catch(error => { + expect(error.code).toEqual(136); + done(); + }); + }); + + it('create fails when installationId is the Delete op (no real ID provided)', done => { + const input = { + installationId: { __op: 'Delete' }, + deviceType: 'ios', + }; + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => { + fail('Creating the installation should have failed.'); + done(); + }) + .catch(error => { + expect(error.code).toEqual(135); + done(); + }); + }); + + it('create fails when installationId is null (no real ID provided)', done => { + const input = { + installationId: null, + deviceType: 'ios', + }; + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => { + fail('Creating the installation should have failed.'); + done(); + }) + .catch(error => { + expect(error.code).toEqual(135); + done(); + }); + }); + + it('master key cannot clear installationId', done => { + const installId = '12345678-abcd-abcd-abcd-123456789abc'; + const input = { + installationId: installId, + deviceType: 'ios', + }; + rest + .create(config, auth.master(config), '_Installation', input) + .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(results => { + expect(results.length).toEqual(1); + return rest.update( + config, + auth.master(config), + '_Installation', + { objectId: results[0].objectId }, + { installationId: { __op: 'Delete' } } + ); + }) + .then(() => { + fail('Master key clearing of installationId should have been rejected.'); + done(); + }) + .catch(error => { + expect(error.code).toEqual(136); + 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..d38813ad29 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1295,6 +1295,27 @@ RestWrite.prototype.handleInstallation = function () { return; } + // installationId is the row's primary identity (used by the SDK auth + // header to bind a client request to its row). Reject any attempt to + // clear it via null or { __op: 'Delete' } before the lookup logic + // below runs — { __op: 'Delete' } would otherwise crash on + // `.toLowerCase()` (TypeError → 500) and null would silently orphan + // the row. Mirrors the existing 136 guard against changing + // installationId from one value to another. + const clearingInstallationId = + this.data.installationId === null || + (typeof this.data.installationId === 'object' && + this.data.installationId !== null && + this.data.installationId.__op === 'Delete'); + if (clearingInstallationId) { + if (this.query) { + throw new Parse.Error(136, 'installationId may not be changed in this operation'); + } + // Create path: drop the operator/null so the "must specify ID" + // guard below fires with the correct 135 error. + delete this.data.installationId; + } + if ( !this.query && !this.data.deviceToken &&