Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions spec/ParseInstallation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const Config = require('../lib/Config');
const Parse = require('parse/node').Parse;
const rest = require('../lib/rest');
const request = require('../lib/request');
const { applyDeviceTokenExists } = require('../lib/Push/utils');

let config;
let database;
Expand Down Expand Up @@ -544,6 +545,145 @@ 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).toBeUndefined();
expect(results[0].installationId).toEqual(installId);
done();
})
.catch(err => {
jfail(err);
done();
});
});

it('removes an installation cleared via null from push recipients', done => {
const installId = '12345678-abcd-abcd-abcd-123456789abc';
const t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306';
const input = {
installationId: installId,
deviceType: 'ios',
deviceToken: t,
};
// The push pipeline selects recipients with `deviceToken: { $exists: true }`,
// which a stored null would still satisfy.
const pushRecipientQuery = applyDeviceTokenExists({});
rest
.create(config, auth.nobody(config), '_Installation', input)
.then(() => database.adapter.find('_Installation', installationSchema, pushRecipientQuery, {}))
.then(results => {
expect(results.length).toEqual(1);
return rest.update(
config,
auth.nobody(config),
'_Installation',
{ installationId: installId },
{ deviceToken: null }
);
})
.then(() => database.adapter.find('_Installation', installationSchema, pushRecipientQuery, {}))
.then(results => {
expect(results.length).toEqual(0);
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 = {
Expand Down
43 changes: 31 additions & 12 deletions src/RestWrite.js
Original file line number Diff line number Diff line change
Expand Up @@ -1295,9 +1295,27 @@ 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;

// Collapse the null form onto the delete form so the field is removed rather
// than stored as null. A stored null still satisfies the `$exists: true`
// filter that selects push recipients, which would keep the installation
// addressable and hand an invalid token to the push adapter.
if (clearingDeviceToken) {
this.data.deviceToken = { __op: 'Delete' };
}

if (
!this.query &&
!this.data.deviceToken &&
!deviceTokenForLookup &&
!this.data.installationId &&
!this.auth.installationId
) {
Expand All @@ -1309,8 +1327,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
Expand All @@ -1330,7 +1349,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;
}

Expand All @@ -1353,8 +1372,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) {
Expand All @@ -1379,7 +1398,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);
}
});
Expand All @@ -1397,9 +1416,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
) {
Expand Down Expand Up @@ -1451,7 +1470,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,
},
Expand Down Expand Up @@ -1486,12 +1505,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
Expand Down