Skip to content

Commit

Permalink
fix: Parse.Installation not working when installation is deleted on…
Browse files Browse the repository at this point in the history
… server (#2126)
  • Loading branch information
dplewis authored May 16, 2024
1 parent f673df6 commit 22360b4
Show file tree
Hide file tree
Showing 3 changed files with 213 additions and 2 deletions.
32 changes: 32 additions & 0 deletions integration/test/ParseUserTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,38 @@ describe('Parse User', () => {
});
});

it('can save new installation when deleted', async () => {
const currentInstallationId = await Parse.CoreManager.getInstallationController().currentInstallationId();
const installation = await Parse.Installation.currentInstallation();
expect(installation.installationId).toBe(currentInstallationId);
expect(installation.deviceType).toBe(Parse.Installation.DEVICE_TYPES.WEB);
await installation.save();
expect(installation.id).toBeDefined();
const objectId = installation.id;
await installation.destroy({ useMasterKey: true });
await installation.save();
expect(installation.id).toBeDefined();
expect(installation.id).not.toBe(objectId);
const currentInstallation = await Parse.Installation.currentInstallation();
expect(currentInstallation.id).toBe(installation.id);
});

it('can fetch installation when deleted', async () => {
const currentInstallationId = await Parse.CoreManager.getInstallationController().currentInstallationId();
const installation = await Parse.Installation.currentInstallation();
expect(installation.installationId).toBe(currentInstallationId);
expect(installation.deviceType).toBe(Parse.Installation.DEVICE_TYPES.WEB);
await installation.save();
expect(installation.id).toBeDefined();
const objectId = installation.id;
await installation.destroy({ useMasterKey: true });
await installation.fetch();
expect(installation.id).toBeDefined();
expect(installation.id).not.toBe(objectId);
const currentInstallation = await Parse.Installation.currentInstallation();
expect(currentInstallation.id).toBe(installation.id);
});

it('can login with userId', async () => {
Parse.User.enableUnsafeCurrentUser();

Expand Down
49 changes: 47 additions & 2 deletions src/ParseInstallation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import CoreManager from './CoreManager';
import ParseError from './ParseError';
import ParseObject from './ParseObject';

import type { AttributeMap } from './ObjectStateMutations';
Expand Down Expand Up @@ -197,17 +198,61 @@ class ParseInstallation extends ParseObject {
}

/**
* Wrap the default save behavior with functionality to save to local storage.
* Wrap the default fetch behavior with functionality to update local storage.
* If the installation is deleted on the server, retry the fetch as a save operation.
*
* @param {...any} args
* @returns {Promise}
*/
async fetch(...args: Array<any>): Promise<ParseInstallation> {
try {
await super.fetch.apply(this, args);
} catch (e) {
if (e.code !== ParseError.OBJECT_NOT_FOUND) {
throw e;
}
// The installation was deleted from the server.
// We always want fetch to succeed.
delete this.id;
this._getId(); // Generate localId
this._markAllFieldsDirty();
await super.save.apply(this, args);
}
await CoreManager.getInstallationController().updateInstallationOnDisk(this);
return this;
}

/**
* Wrap the default save behavior with functionality to update the local storage.
* If the installation is deleted on the server, retry saving a new installation.
*
* @param {...any} args
* @returns {Promise}
*/
async save(...args: Array<any>): Promise<this> {
await super.save.apply(this, args);
try {
await super.save.apply(this, args);
} catch (e) {
if (e.code !== ParseError.OBJECT_NOT_FOUND) {
throw e;
}
// The installation was deleted from the server.
// We always want save to succeed.
delete this.id;
this._getId(); // Generate localId
this._markAllFieldsDirty();
await super.save.apply(this, args);
}
await CoreManager.getInstallationController().updateInstallationOnDisk(this);
return this;
}

_markAllFieldsDirty() {
for (const [key, value] of Object.entries(this.attributes)) {
this.set(key, value);
}
}

/**
* Get the current Parse.Installation from disk. If doesn't exists, create an new installation.
*
Expand Down
134 changes: 134 additions & 0 deletions src/__tests__/ParseInstallation-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jest.dontMock('../TaskQueue');
jest.dontMock('../SingleInstanceStateController');
jest.dontMock('../UniqueInstanceStateController');

const ParseError = require('../ParseError').default;
const LocalDatastore = require('../LocalDatastore');
const ParseInstallation = require('../ParseInstallation');
const CoreManager = require('../CoreManager');
Expand Down Expand Up @@ -84,6 +85,67 @@ describe('ParseInstallation', () => {
expect(InstallationController.updateInstallationOnDisk).toHaveBeenCalledTimes(1);
});

it('can save if object not found', async () => {
const InstallationController = {
async updateInstallationOnDisk() {},
async currentInstallationId() {},
async currentInstallation() {},
};
let once = true; // save will be called twice first time will reject
CoreManager.setInstallationController(InstallationController);
CoreManager.setRESTController({
request() {
if (!once) {
return Promise.resolve({}, 200);
}
once = false;
const parseError = new ParseError(
ParseError.OBJECT_NOT_FOUND,
'Object not found.'
);
return Promise.reject(parseError);
},
ajax() {},
});
CoreManager.setLocalDatastore(LocalDatastore);
jest.spyOn(InstallationController, 'updateInstallationOnDisk').mockImplementationOnce(() => {});
const installation = new ParseInstallation();
installation.set('deviceToken', '1234');
jest.spyOn(installation, '_markAllFieldsDirty');
await installation.save();
expect(installation._markAllFieldsDirty).toHaveBeenCalledTimes(1);
expect(InstallationController.updateInstallationOnDisk).toHaveBeenCalledTimes(1);
});

it('can save and handle errors', async () => {
const InstallationController = {
async updateInstallationOnDisk() {},
async currentInstallationId() {},
async currentInstallation() {},
};
CoreManager.setInstallationController(InstallationController);
CoreManager.setRESTController({
request() {
const parseError = new ParseError(
ParseError.INTERNAL_SERVER_ERROR,
'Cannot save installation on client.'
);
return Promise.reject(parseError);
},
ajax() {},
});
CoreManager.setLocalDatastore(LocalDatastore);
jest.spyOn(InstallationController, 'updateInstallationOnDisk').mockImplementationOnce(() => {});
const installation = new ParseInstallation();
installation.set('deviceToken', '1234');
try {
await installation.save();
} catch (e) {
expect(e.message).toEqual('Cannot save installation on client.');
}
expect(InstallationController.updateInstallationOnDisk).toHaveBeenCalledTimes(0);
});

it('can get current installation', async () => {
const InstallationController = {
async updateInstallationOnDisk() {},
Expand All @@ -100,4 +162,76 @@ describe('ParseInstallation', () => {
expect(installation.deviceType).toEqual('web');
expect(installation.installationId).toEqual('1234');
});

it('can fetch and save to disk', async () => {
const InstallationController = {
async updateInstallationOnDisk() {},
async currentInstallationId() {},
async currentInstallation() {},
};
CoreManager.setInstallationController(InstallationController);
CoreManager.setRESTController({
request() {
return Promise.resolve({}, 200);
},
ajax() {},
});
CoreManager.setLocalDatastore(LocalDatastore);
jest.spyOn(InstallationController, 'updateInstallationOnDisk').mockImplementationOnce(() => {});
const installation = new ParseInstallation();
installation.id = 'abc';
await installation.fetch();
expect(InstallationController.updateInstallationOnDisk).toHaveBeenCalledTimes(1);
});

it('can fetch if object not found', async () => {
const InstallationController = {
async updateInstallationOnDisk() {},
async currentInstallationId() {},
async currentInstallation() {},
};
let once = true;
CoreManager.setInstallationController(InstallationController);
CoreManager.setRESTController({
request() {
if (!once) {
// save() results
return Promise.resolve({}, 200);
}
once = false;
// fetch() results
const parseError = new ParseError(
ParseError.OBJECT_NOT_FOUND,
'Object not found.'
);
return Promise.reject(parseError);
},
ajax() {},
});
CoreManager.setLocalDatastore(LocalDatastore);
jest.spyOn(InstallationController, 'updateInstallationOnDisk').mockImplementationOnce(() => {});
const installation = new ParseInstallation();
installation.id = '1234';
jest.spyOn(installation, '_markAllFieldsDirty');
await installation.fetch();
expect(installation._markAllFieldsDirty).toHaveBeenCalledTimes(1);
expect(InstallationController.updateInstallationOnDisk).toHaveBeenCalledTimes(1);
});

it('can fetch and handle errors', async () => {
const InstallationController = {
async updateInstallationOnDisk() {},
async currentInstallationId() {},
async currentInstallation() {},
};
CoreManager.setInstallationController(InstallationController);
jest.spyOn(InstallationController, 'updateInstallationOnDisk').mockImplementationOnce(() => {});
const installation = new ParseInstallation();
try {
await installation.fetch();
} catch (e) {
expect(e.message).toEqual('Object does not have an ID');
}
expect(InstallationController.updateInstallationOnDisk).toHaveBeenCalledTimes(0);
});
});

0 comments on commit 22360b4

Please sign in to comment.