Skip to content

fix: Parse.Installation not working when installation is deleted on server #2126

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 16, 2024
Merged
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
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);
});
});
Loading