Skip to content

Commit 22360b4

Browse files
authored
fix: Parse.Installation not working when installation is deleted on server (#2126)
1 parent f673df6 commit 22360b4

File tree

3 files changed

+213
-2
lines changed

3 files changed

+213
-2
lines changed

integration/test/ParseUserTest.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,38 @@ describe('Parse User', () => {
179179
});
180180
});
181181

182+
it('can save new installation when deleted', async () => {
183+
const currentInstallationId = await Parse.CoreManager.getInstallationController().currentInstallationId();
184+
const installation = await Parse.Installation.currentInstallation();
185+
expect(installation.installationId).toBe(currentInstallationId);
186+
expect(installation.deviceType).toBe(Parse.Installation.DEVICE_TYPES.WEB);
187+
await installation.save();
188+
expect(installation.id).toBeDefined();
189+
const objectId = installation.id;
190+
await installation.destroy({ useMasterKey: true });
191+
await installation.save();
192+
expect(installation.id).toBeDefined();
193+
expect(installation.id).not.toBe(objectId);
194+
const currentInstallation = await Parse.Installation.currentInstallation();
195+
expect(currentInstallation.id).toBe(installation.id);
196+
});
197+
198+
it('can fetch installation when deleted', async () => {
199+
const currentInstallationId = await Parse.CoreManager.getInstallationController().currentInstallationId();
200+
const installation = await Parse.Installation.currentInstallation();
201+
expect(installation.installationId).toBe(currentInstallationId);
202+
expect(installation.deviceType).toBe(Parse.Installation.DEVICE_TYPES.WEB);
203+
await installation.save();
204+
expect(installation.id).toBeDefined();
205+
const objectId = installation.id;
206+
await installation.destroy({ useMasterKey: true });
207+
await installation.fetch();
208+
expect(installation.id).toBeDefined();
209+
expect(installation.id).not.toBe(objectId);
210+
const currentInstallation = await Parse.Installation.currentInstallation();
211+
expect(currentInstallation.id).toBe(installation.id);
212+
});
213+
182214
it('can login with userId', async () => {
183215
Parse.User.enableUnsafeCurrentUser();
184216

src/ParseInstallation.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import CoreManager from './CoreManager';
2+
import ParseError from './ParseError';
23
import ParseObject from './ParseObject';
34

45
import type { AttributeMap } from './ObjectStateMutations';
@@ -197,17 +198,61 @@ class ParseInstallation extends ParseObject {
197198
}
198199

199200
/**
200-
* Wrap the default save behavior with functionality to save to local storage.
201+
* Wrap the default fetch behavior with functionality to update local storage.
202+
* If the installation is deleted on the server, retry the fetch as a save operation.
203+
*
204+
* @param {...any} args
205+
* @returns {Promise}
206+
*/
207+
async fetch(...args: Array<any>): Promise<ParseInstallation> {
208+
try {
209+
await super.fetch.apply(this, args);
210+
} catch (e) {
211+
if (e.code !== ParseError.OBJECT_NOT_FOUND) {
212+
throw e;
213+
}
214+
// The installation was deleted from the server.
215+
// We always want fetch to succeed.
216+
delete this.id;
217+
this._getId(); // Generate localId
218+
this._markAllFieldsDirty();
219+
await super.save.apply(this, args);
220+
}
221+
await CoreManager.getInstallationController().updateInstallationOnDisk(this);
222+
return this;
223+
}
224+
225+
/**
226+
* Wrap the default save behavior with functionality to update the local storage.
227+
* If the installation is deleted on the server, retry saving a new installation.
201228
*
202229
* @param {...any} args
203230
* @returns {Promise}
204231
*/
205232
async save(...args: Array<any>): Promise<this> {
206-
await super.save.apply(this, args);
233+
try {
234+
await super.save.apply(this, args);
235+
} catch (e) {
236+
if (e.code !== ParseError.OBJECT_NOT_FOUND) {
237+
throw e;
238+
}
239+
// The installation was deleted from the server.
240+
// We always want save to succeed.
241+
delete this.id;
242+
this._getId(); // Generate localId
243+
this._markAllFieldsDirty();
244+
await super.save.apply(this, args);
245+
}
207246
await CoreManager.getInstallationController().updateInstallationOnDisk(this);
208247
return this;
209248
}
210249

250+
_markAllFieldsDirty() {
251+
for (const [key, value] of Object.entries(this.attributes)) {
252+
this.set(key, value);
253+
}
254+
}
255+
211256
/**
212257
* Get the current Parse.Installation from disk. If doesn't exists, create an new installation.
213258
*

src/__tests__/ParseInstallation-test.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ jest.dontMock('../TaskQueue');
1212
jest.dontMock('../SingleInstanceStateController');
1313
jest.dontMock('../UniqueInstanceStateController');
1414

15+
const ParseError = require('../ParseError').default;
1516
const LocalDatastore = require('../LocalDatastore');
1617
const ParseInstallation = require('../ParseInstallation');
1718
const CoreManager = require('../CoreManager');
@@ -84,6 +85,67 @@ describe('ParseInstallation', () => {
8485
expect(InstallationController.updateInstallationOnDisk).toHaveBeenCalledTimes(1);
8586
});
8687

88+
it('can save if object not found', async () => {
89+
const InstallationController = {
90+
async updateInstallationOnDisk() {},
91+
async currentInstallationId() {},
92+
async currentInstallation() {},
93+
};
94+
let once = true; // save will be called twice first time will reject
95+
CoreManager.setInstallationController(InstallationController);
96+
CoreManager.setRESTController({
97+
request() {
98+
if (!once) {
99+
return Promise.resolve({}, 200);
100+
}
101+
once = false;
102+
const parseError = new ParseError(
103+
ParseError.OBJECT_NOT_FOUND,
104+
'Object not found.'
105+
);
106+
return Promise.reject(parseError);
107+
},
108+
ajax() {},
109+
});
110+
CoreManager.setLocalDatastore(LocalDatastore);
111+
jest.spyOn(InstallationController, 'updateInstallationOnDisk').mockImplementationOnce(() => {});
112+
const installation = new ParseInstallation();
113+
installation.set('deviceToken', '1234');
114+
jest.spyOn(installation, '_markAllFieldsDirty');
115+
await installation.save();
116+
expect(installation._markAllFieldsDirty).toHaveBeenCalledTimes(1);
117+
expect(InstallationController.updateInstallationOnDisk).toHaveBeenCalledTimes(1);
118+
});
119+
120+
it('can save and handle errors', async () => {
121+
const InstallationController = {
122+
async updateInstallationOnDisk() {},
123+
async currentInstallationId() {},
124+
async currentInstallation() {},
125+
};
126+
CoreManager.setInstallationController(InstallationController);
127+
CoreManager.setRESTController({
128+
request() {
129+
const parseError = new ParseError(
130+
ParseError.INTERNAL_SERVER_ERROR,
131+
'Cannot save installation on client.'
132+
);
133+
return Promise.reject(parseError);
134+
},
135+
ajax() {},
136+
});
137+
CoreManager.setLocalDatastore(LocalDatastore);
138+
jest.spyOn(InstallationController, 'updateInstallationOnDisk').mockImplementationOnce(() => {});
139+
const installation = new ParseInstallation();
140+
installation.set('deviceToken', '1234');
141+
try {
142+
await installation.save();
143+
} catch (e) {
144+
expect(e.message).toEqual('Cannot save installation on client.');
145+
}
146+
expect(InstallationController.updateInstallationOnDisk).toHaveBeenCalledTimes(0);
147+
});
148+
87149
it('can get current installation', async () => {
88150
const InstallationController = {
89151
async updateInstallationOnDisk() {},
@@ -100,4 +162,76 @@ describe('ParseInstallation', () => {
100162
expect(installation.deviceType).toEqual('web');
101163
expect(installation.installationId).toEqual('1234');
102164
});
165+
166+
it('can fetch and save to disk', async () => {
167+
const InstallationController = {
168+
async updateInstallationOnDisk() {},
169+
async currentInstallationId() {},
170+
async currentInstallation() {},
171+
};
172+
CoreManager.setInstallationController(InstallationController);
173+
CoreManager.setRESTController({
174+
request() {
175+
return Promise.resolve({}, 200);
176+
},
177+
ajax() {},
178+
});
179+
CoreManager.setLocalDatastore(LocalDatastore);
180+
jest.spyOn(InstallationController, 'updateInstallationOnDisk').mockImplementationOnce(() => {});
181+
const installation = new ParseInstallation();
182+
installation.id = 'abc';
183+
await installation.fetch();
184+
expect(InstallationController.updateInstallationOnDisk).toHaveBeenCalledTimes(1);
185+
});
186+
187+
it('can fetch if object not found', async () => {
188+
const InstallationController = {
189+
async updateInstallationOnDisk() {},
190+
async currentInstallationId() {},
191+
async currentInstallation() {},
192+
};
193+
let once = true;
194+
CoreManager.setInstallationController(InstallationController);
195+
CoreManager.setRESTController({
196+
request() {
197+
if (!once) {
198+
// save() results
199+
return Promise.resolve({}, 200);
200+
}
201+
once = false;
202+
// fetch() results
203+
const parseError = new ParseError(
204+
ParseError.OBJECT_NOT_FOUND,
205+
'Object not found.'
206+
);
207+
return Promise.reject(parseError);
208+
},
209+
ajax() {},
210+
});
211+
CoreManager.setLocalDatastore(LocalDatastore);
212+
jest.spyOn(InstallationController, 'updateInstallationOnDisk').mockImplementationOnce(() => {});
213+
const installation = new ParseInstallation();
214+
installation.id = '1234';
215+
jest.spyOn(installation, '_markAllFieldsDirty');
216+
await installation.fetch();
217+
expect(installation._markAllFieldsDirty).toHaveBeenCalledTimes(1);
218+
expect(InstallationController.updateInstallationOnDisk).toHaveBeenCalledTimes(1);
219+
});
220+
221+
it('can fetch and handle errors', async () => {
222+
const InstallationController = {
223+
async updateInstallationOnDisk() {},
224+
async currentInstallationId() {},
225+
async currentInstallation() {},
226+
};
227+
CoreManager.setInstallationController(InstallationController);
228+
jest.spyOn(InstallationController, 'updateInstallationOnDisk').mockImplementationOnce(() => {});
229+
const installation = new ParseInstallation();
230+
try {
231+
await installation.fetch();
232+
} catch (e) {
233+
expect(e.message).toEqual('Object does not have an ID');
234+
}
235+
expect(InstallationController.updateInstallationOnDisk).toHaveBeenCalledTimes(0);
236+
});
103237
});

0 commit comments

Comments
 (0)