diff --git a/src/LiveQueryClient.js b/src/LiveQueryClient.js index 20bfdb014..108c2edca 100644 --- a/src/LiveQueryClient.js +++ b/src/LiveQueryClient.js @@ -167,6 +167,10 @@ class LiveQueryClient extends EventEmitter { this.connectPromise = resolvingPromise(); this.subscriptions = new Map(); this.state = CLIENT_STATE.INITIALIZED; + + // adding listener so process does not crash + // best practice is for developer to register their own listener + this.on('error', () => {}); } shouldOpen(): any { @@ -447,7 +451,7 @@ class LiveQueryClient extends EventEmitter { _handleWebSocketError(error: any) { this.emit(CLIENT_EMMITER_TYPES.ERROR, error); for (const subscription of this.subscriptions.values()) { - subscription.emit(SUBSCRIPTION_EMMITER_TYPES.ERROR); + subscription.emit(SUBSCRIPTION_EMMITER_TYPES.ERROR, error); } this._handleReconnect(); } diff --git a/src/ParseACL.js b/src/ParseACL.js index 705b161d2..c756fec25 100644 --- a/src/ParseACL.js +++ b/src/ParseACL.js @@ -46,11 +46,6 @@ class ParseACL { } else { for (const userId in arg1) { const accessList = arg1[userId]; - if (typeof userId !== 'string') { - throw new TypeError( - 'Tried to create an ACL with an invalid user id.' - ); - } this.permissionsById[userId] = {}; for (const permission in accessList) { const allowed = accessList[permission]; diff --git a/src/ParseConfig.js b/src/ParseConfig.js index 0cee5bf71..ebd11883b 100644 --- a/src/ParseConfig.js +++ b/src/ParseConfig.js @@ -116,6 +116,15 @@ class ParseConfig { return Promise.reject(error); }); } + + /** + * Used for testing + * + * @private + */ + static _clearCache() { + currentConfig = null; + } } let currentConfig = null; @@ -141,9 +150,8 @@ const DefaultController = { const config = new ParseConfig(); const storagePath = Storage.generatePath(CURRENT_CONFIG_KEY); - let configData; if (!Storage.async()) { - configData = Storage.getItem(storagePath); + const configData = Storage.getItem(storagePath); if (configData) { const attributes = decodePayload(configData); diff --git a/src/ParseFile.js b/src/ParseFile.js index bd105fc01..ec0a236c6 100644 --- a/src/ParseFile.js +++ b/src/ParseFile.js @@ -441,9 +441,9 @@ const DefaultController = { const base64Data = await new Promise((res, rej) => { // eslint-disable-next-line no-undef const reader = new FileReader(); - reader.readAsDataURL(source.file); reader.onload = () => res(reader.result); reader.onerror = error => rej(error); + reader.readAsDataURL(source.file); }); // we only want the data after the comma // For example: "data:application/pdf;base64,JVBERi0xLjQKJ..." we would only want "JVBERi0xLjQKJ..." @@ -556,8 +556,13 @@ const DefaultController = { _setXHR(xhr: any) { XHR = xhr; }, + + _getXHR() { + return XHR; + }, }; CoreManager.setFileController(DefaultController); export default ParseFile; +exports.b64Digit = b64Digit; diff --git a/src/ParseInstallation.js b/src/ParseInstallation.js index 74b198050..a701faf2e 100644 --- a/src/ParseInstallation.js +++ b/src/ParseInstallation.js @@ -18,7 +18,7 @@ export default class Installation extends ParseObject { super('_Installation'); if (attributes && typeof attributes === 'object'){ if (!this.set(attributes || {})) { - throw new Error('Can\'t create an invalid Session'); + throw new Error('Can\'t create an invalid Installation'); } } } diff --git a/src/ParseQuery.js b/src/ParseQuery.js index f47f9e1c0..93712a57b 100644 --- a/src/ParseQuery.js +++ b/src/ParseQuery.js @@ -820,7 +820,9 @@ class ParseQuery { } if (Object.keys(this._where || {}).length) { - if(!Array.isArray(pipeline)) pipeline = [pipeline]; + if (!Array.isArray(pipeline)) { + pipeline = [pipeline]; + } pipeline.unshift({ match: this._where }); } diff --git a/src/ParseUser.js b/src/ParseUser.js index 35043a093..115eeff1b 100644 --- a/src/ParseUser.js +++ b/src/ParseUser.js @@ -105,7 +105,7 @@ class ParseUser extends ParseObject { return authType; }, }; - authProviders[authType] = authProvider; + authProviders[authProvider.getAuthType()] = authProvider; provider = authProvider; } } else { diff --git a/src/Socket.weapp.js b/src/Socket.weapp.js index d3f757fe5..bd56b8151 100644 --- a/src/Socket.weapp.js +++ b/src/Socket.weapp.js @@ -5,10 +5,6 @@ module.exports = class SocketWeapp { this.onclose = () => {} this.onerror = () => {} - wx.connectSocket({ - url: serverURL - }) - wx.onSocketOpen(() => { this.onopen(); }) @@ -19,11 +15,15 @@ module.exports = class SocketWeapp { wx.onSocketClose(() => { this.onclose(); - }) + }); wx.onSocketError((error) => { this.onerror(error); - }) + }); + + wx.connectSocket({ + url: serverURL, + }); } send(data) { diff --git a/src/__tests__/Cloud-test.js b/src/__tests__/Cloud-test.js index b6f7b98ab..14d5d42b4 100644 --- a/src/__tests__/Cloud-test.js +++ b/src/__tests__/Cloud-test.js @@ -11,6 +11,9 @@ jest.dontMock('../Cloud'); jest.dontMock('../CoreManager'); jest.dontMock('../decode'); jest.dontMock('../encode'); +jest.dontMock('../ParseError'); +jest.dontMock('../ParseObject'); +jest.dontMock('../ParseQuery'); const Cloud = require('../Cloud'); const CoreManager = require('../CoreManager'); @@ -222,4 +225,24 @@ describe('CloudController', () => { // Validate expect(controller.request.mock.calls[0][3].context).toEqual(context); }); + + it('can get job status', async () => { + const request = jest.fn(); + request.mockReturnValue(Promise.resolve({ + results: [{ className: '_JobStatus', objectId: 'jobId1234' }], + })); + CoreManager.setRESTController({ request: request, ajax: jest.fn() }); + + await Cloud.getJobStatus('jobId1234'); + const [ method, path, data, options] = request.mock.calls[0]; + expect(method).toBe('GET'); + expect(path).toBe('classes/_JobStatus'); + expect(data).toEqual({ + limit: 1, + where: { + objectId: 'jobId1234', + }, + }); + expect(options.useMasterKey).toBe(true); + }); }); diff --git a/src/__tests__/Hooks-test.js b/src/__tests__/Hooks-test.js index c81614f33..a67f3dbd5 100644 --- a/src/__tests__/Hooks-test.js +++ b/src/__tests__/Hooks-test.js @@ -11,11 +11,13 @@ jest.dontMock('../ParseHooks'); jest.dontMock('../CoreManager'); jest.dontMock('../decode'); jest.dontMock('../encode'); +jest.dontMock('../ParseError'); const Hooks = require('../ParseHooks'); const CoreManager = require('../CoreManager'); const defaultController = CoreManager.getHooksController(); +const { sendRequest } = defaultController; describe('Hooks', () => { beforeEach(() => { @@ -200,5 +202,27 @@ describe('Hooks', () => { done(); }) + it('should sendRequest', async () => { + defaultController.sendRequest = sendRequest; + const request = function() { + return Promise.resolve(12); + }; + CoreManager.setRESTController({ request, ajax: jest.fn() }); + const decoded = await defaultController.sendRequest('POST', 'hooks/triggers/myhook'); + expect(decoded).toBe(12); + }); + it('handle sendRequest error', async () => { + defaultController.sendRequest = sendRequest; + const request = function() { + return Promise.resolve(undefined); + }; + CoreManager.setRESTController({ request, ajax: jest.fn() }); + try { + await defaultController.sendRequest('POST', 'hooks/triggers/myhook'); + expect(false).toBe(true); + } catch (e) { + expect(e.message).toBe('The server returned an invalid response.'); + } + }); }); diff --git a/src/__tests__/InstallationController-test.js b/src/__tests__/InstallationController-test.js index 51adfc4ae..9a672208b 100644 --- a/src/__tests__/InstallationController-test.js +++ b/src/__tests__/InstallationController-test.js @@ -59,4 +59,13 @@ describe('InstallationController', () => { done(); }); }); + + it('can set installation id', (done) => { + const iid = '12345678'; + InstallationController._setInstallationIdCache(iid); + InstallationController.currentInstallationId().then((i) => { + expect(i).toBe(iid); + done(); + }); + }); }); diff --git a/src/__tests__/LiveQueryClient-test.js b/src/__tests__/LiveQueryClient-test.js index 844620c49..ae52d92bd 100644 --- a/src/__tests__/LiveQueryClient-test.js +++ b/src/__tests__/LiveQueryClient-test.js @@ -47,27 +47,93 @@ const CoreManager = require('../CoreManager'); const LiveQueryClient = require('../LiveQueryClient').default; const ParseObject = require('../ParseObject').default; const ParseQuery = require('../ParseQuery').default; +const { resolvingPromise } = require('../promiseUtils'); const events = require('events'); CoreManager.setLocalDatastore(mockLocalDatastore); -function resolvingPromise() { - let res; - let rej; - const promise = new Promise((resolve, reject) => { - res = resolve; - rej = reject; - }); - promise.resolve = res; - promise.reject = rej; - return promise; -} - describe('LiveQueryClient', () => { beforeEach(() => { mockLocalDatastore.isEnabled = false; }); + it('serverURL required', () => { + expect(() => { + new LiveQueryClient({}); + }).toThrow('You need to set a proper Parse LiveQuery server url before using LiveQueryClient'); + }); + + it('WebSocketController required', (done) => { + const WebSocketImplementation = CoreManager.getWebSocketController(); + CoreManager.setWebSocketController(); + const liveQueryClient = new LiveQueryClient({ + applicationId: 'applicationId', + serverURL: 'ws://test', + javascriptKey: 'javascriptKey', + masterKey: 'masterKey', + sessionToken: 'sessionToken' + }); + liveQueryClient.on('error', (error) => { + expect(error).toBe('Can not find WebSocket implementation'); + CoreManager.setWebSocketController(WebSocketImplementation); + done(); + }) + liveQueryClient.open(); + }); + + it('can unsubscribe', async () => { + const liveQueryClient = new LiveQueryClient({ + applicationId: 'applicationId', + serverURL: 'ws://test', + javascriptKey: 'javascriptKey', + masterKey: 'masterKey', + sessionToken: 'sessionToken' + }); + liveQueryClient.socket = { + send: jest.fn() + }; + const subscription = { + id: 1 + } + liveQueryClient.subscriptions.set(1, subscription); + + liveQueryClient.unsubscribe(subscription); + liveQueryClient.connectPromise.resolve(); + expect(liveQueryClient.subscriptions.size).toBe(0); + await liveQueryClient.connectPromise; + const messageStr = liveQueryClient.socket.send.mock.calls[0][0]; + const message = JSON.parse(messageStr); + expect(message).toEqual({ + op: 'unsubscribe', + requestId: 1 + }); + }); + + it('can handle open / close states', () => { + const liveQueryClient = new LiveQueryClient({ + applicationId: 'applicationId', + serverURL: 'ws://test', + javascriptKey: 'javascriptKey', + masterKey: 'masterKey', + sessionToken: 'sessionToken' + }); + expect(liveQueryClient.shouldOpen()).toBe(true); + liveQueryClient.close(); + expect(liveQueryClient.shouldOpen()).toBe(true); + liveQueryClient.open(); + expect(liveQueryClient.shouldOpen()).toBe(false); + }); + + it('set undefined sessionToken default', () => { + const liveQueryClient = new LiveQueryClient({ + applicationId: 'applicationId', + serverURL: 'ws://test', + javascriptKey: 'javascriptKey', + masterKey: 'masterKey', + }); + expect(liveQueryClient.sessionToken).toBe(undefined); + }); + it('can connect to server', () => { const liveQueryClient = new LiveQueryClient({ applicationId: 'applicationId', @@ -154,6 +220,37 @@ describe('LiveQueryClient', () => { expect(liveQueryClient.state).toEqual('connected'); }); + it('can handle WebSocket reconnect on connected response message', async () => { + const liveQueryClient = new LiveQueryClient({ + applicationId: 'applicationId', + serverURL: 'ws://test', + javascriptKey: 'javascriptKey', + masterKey: 'masterKey', + sessionToken: 'sessionToken' + }); + const data = { + op: 'connected', + clientId: 1 + }; + const event = { + data: JSON.stringify(data) + } + // Register checked in advance + let isChecked = false; + liveQueryClient.on('open', function() { + isChecked = true; + }); + jest.spyOn(liveQueryClient, 'resubscribe'); + liveQueryClient._handleReconnect(); + liveQueryClient._handleWebSocketMessage(event); + + expect(isChecked).toBe(true); + expect(liveQueryClient.id).toBe(1); + await liveQueryClient.connectPromise; + expect(liveQueryClient.state).toEqual('connected'); + expect(liveQueryClient.resubscribe).toHaveBeenCalledTimes(1); + }); + it('can handle WebSocket subscribed response message', () => { const liveQueryClient = new LiveQueryClient({ applicationId: 'applicationId', @@ -186,6 +283,30 @@ describe('LiveQueryClient', () => { expect(isChecked).toBe(true); }); + it('can handle WebSocket unsubscribed response message', () => { + const liveQueryClient = new LiveQueryClient({ + applicationId: 'applicationId', + serverURL: 'ws://test', + javascriptKey: 'javascriptKey', + masterKey: 'masterKey', + sessionToken: 'sessionToken' + }); + const subscription = new events.EventEmitter(); + subscription.subscribePromise = resolvingPromise(); + + liveQueryClient.subscriptions.set(1, subscription); + const data = { + op: 'unsubscribed', + clientId: 1, + requestId: 1 + }; + const event = { + data: JSON.stringify(data) + } + liveQueryClient._handleWebSocketMessage(event); + expect(liveQueryClient.subscriptions.size).toBe(1); + }); + it('can handle WebSocket error response message', () => { const liveQueryClient = new LiveQueryClient({ applicationId: 'applicationId', @@ -284,6 +405,28 @@ describe('LiveQueryClient', () => { expect(isChecked).toBe(true); }); + it('can handle WebSocket event response message without subscription', () => { + const liveQueryClient = new LiveQueryClient({ + applicationId: 'applicationId', + serverURL: 'ws://test', + javascriptKey: 'javascriptKey', + masterKey: 'masterKey', + sessionToken: 'sessionToken' + }); + const object = new ParseObject('Test'); + object.set('key', 'value'); + const data = { + op: 'create', + clientId: 1, + requestId: 1, + object: object._toFullJSON() + }; + const event = { + data: JSON.stringify(data) + } + liveQueryClient._handleWebSocketMessage(event); + }); + it('can handle WebSocket response with original', () => { const liveQueryClient = new LiveQueryClient({ applicationId: 'applicationId', @@ -450,6 +593,34 @@ describe('LiveQueryClient', () => { expect(isCheckedAgain).toBe(true); }); + it('can handle WebSocket close message while disconnected', () => { + const liveQueryClient = new LiveQueryClient({ + applicationId: 'applicationId', + serverURL: 'ws://test', + javascriptKey: 'javascriptKey', + masterKey: 'masterKey', + sessionToken: 'sessionToken' + }); + // Add mock subscription + const subscription = new events.EventEmitter(); + liveQueryClient.subscriptions.set(1, subscription); + // Register checked in advance + let isChecked = false; + subscription.on('close', function() { + isChecked = true; + }); + let isCheckedAgain = false; + liveQueryClient.on('close', function() { + isCheckedAgain = true; + }); + liveQueryClient.open(); + liveQueryClient.close(); + liveQueryClient._handleWebSocketClose(); + + expect(isChecked).toBe(true); + expect(isCheckedAgain).toBe(true); + }); + it('can handle reconnect', () => { const liveQueryClient = new LiveQueryClient({ applicationId: 'applicationId', @@ -471,6 +642,33 @@ describe('LiveQueryClient', () => { expect(liveQueryClient.open).toBeCalled(); }); + it('can handle reconnect and clear handler', () => { + const liveQueryClient = new LiveQueryClient({ + applicationId: 'applicationId', + serverURL: 'ws://test', + javascriptKey: 'javascriptKey', + masterKey: 'masterKey', + sessionToken: 'sessionToken' + }); + + liveQueryClient.open = jest.fn(); + + const attempts = liveQueryClient.attempts; + liveQueryClient.state = 'disconnected'; + liveQueryClient._handleReconnect(); + expect(liveQueryClient.state).toEqual('disconnected'); + + liveQueryClient.state = 'connected'; + liveQueryClient._handleReconnect(); + expect(liveQueryClient.state).toEqual('reconnecting'); + + liveQueryClient._handleReconnect(); + jest.runOnlyPendingTimers(); + + expect(liveQueryClient.attempts).toEqual(attempts + 1); + expect(liveQueryClient.open).toBeCalled(); + }); + it('can handle WebSocket error message', () => { const liveQueryClient = new LiveQueryClient({ applicationId: 'applicationId', @@ -491,6 +689,25 @@ describe('LiveQueryClient', () => { expect(isChecked).toBe(true); }); + it('can handle WebSocket error message with subscriptions', (done) => { + const liveQueryClient = new LiveQueryClient({ + applicationId: 'applicationId', + serverURL: 'ws://test', + javascriptKey: 'javascriptKey', + masterKey: 'masterKey', + sessionToken: 'sessionToken' + }); + const subscription = new events.EventEmitter(); + liveQueryClient.subscriptions.set(1, subscription); + const error = {} + subscription.on('error', (errorAgain) => { + expect(errorAgain).toEqual(error); + done(); + }); + + liveQueryClient._handleWebSocketError(error); + }); + it('can handle WebSocket reconnect on error event', () => { const liveQueryClient = new LiveQueryClient({ applicationId: 'applicationId', @@ -560,6 +777,45 @@ describe('LiveQueryClient', () => { }); }); + it('can subscribe with sessionToken', async () => { + const liveQueryClient = new LiveQueryClient({ + applicationId: 'applicationId', + serverURL: 'ws://test', + javascriptKey: 'javascriptKey', + masterKey: 'masterKey', + sessionToken: 'sessionToken' + }); + liveQueryClient.socket = { + send: jest.fn() + }; + const query = new ParseQuery('Test'); + query.equalTo('key', 'value'); + + const subscribePromise = liveQueryClient.subscribe(query, 'mySessionToken'); + const clientSub = liveQueryClient.subscriptions.get(1); + clientSub.subscribePromise.resolve(); + + const subscription = await subscribePromise; + liveQueryClient.connectPromise.resolve(); + expect(subscription).toBe(clientSub); + expect(subscription.sessionToken).toBe('mySessionToken'); + expect(liveQueryClient.requestId).toBe(2); + await liveQueryClient.connectPromise; + const messageStr = liveQueryClient.socket.send.mock.calls[0][0]; + const message = JSON.parse(messageStr); + expect(message).toEqual({ + op: 'subscribe', + requestId: 1, + sessionToken: 'mySessionToken', + query: { + className: 'Test', + where: { + key: 'value' + } + } + }); + }); + it('can unsubscribe', async () => { const liveQueryClient = new LiveQueryClient({ applicationId: 'applicationId', @@ -588,6 +844,23 @@ describe('LiveQueryClient', () => { }); }); + it('can unsubscribe without subscription', async () => { + const liveQueryClient = new LiveQueryClient({ + applicationId: 'applicationId', + serverURL: 'ws://test', + javascriptKey: 'javascriptKey', + masterKey: 'masterKey', + sessionToken: 'sessionToken' + }); + liveQueryClient.socket = { + send: jest.fn() + }; + liveQueryClient.unsubscribe(); + liveQueryClient.connectPromise.resolve(); + await liveQueryClient.connectPromise; + expect(liveQueryClient.socket.send).toHaveBeenCalledTimes(0); + }); + it('can resubscribe', async () => { const liveQueryClient = new LiveQueryClient({ applicationId: 'applicationId', @@ -622,6 +895,41 @@ describe('LiveQueryClient', () => { }); }); + it('can resubscribe with sessionToken', async () => { + const liveQueryClient = new LiveQueryClient({ + applicationId: 'applicationId', + serverURL: 'ws://test', + javascriptKey: 'javascriptKey', + masterKey: 'masterKey', + sessionToken: 'sessionToken' + }); + liveQueryClient.socket = { + send: jest.fn() + }; + const query = new ParseQuery('Test'); + query.equalTo('key', 'value'); + liveQueryClient.subscribe(query, 'mySessionToken'); + liveQueryClient.connectPromise.resolve(); + + liveQueryClient.resubscribe(); + + expect(liveQueryClient.requestId).toBe(2); + await liveQueryClient.connectPromise; + const messageStr = liveQueryClient.socket.send.mock.calls[0][0]; + const message = JSON.parse(messageStr); + expect(message).toEqual({ + op: 'subscribe', + requestId: 1, + sessionToken: 'mySessionToken', + query: { + className: 'Test', + where: { + key: 'value' + } + } + }); + }); + it('can close', () => { const liveQueryClient = new LiveQueryClient({ applicationId: 'applicationId', @@ -694,4 +1002,15 @@ describe('LiveQueryClient', () => { expect(isChecked).toBe(true); }); + + it('cannot subscribe without query', () => { + const liveQueryClient = new LiveQueryClient({ + applicationId: 'applicationId', + serverURL: 'ws://test', + javascriptKey: 'javascriptKey', + masterKey: 'masterKey', + }); + const subscription = liveQueryClient.subscribe(); + expect(subscription).toBe(undefined); + }); }); diff --git a/src/__tests__/LocalDatastore-test.js b/src/__tests__/LocalDatastore-test.js index ce7623c91..b0038459f 100644 --- a/src/__tests__/LocalDatastore-test.js +++ b/src/__tests__/LocalDatastore-test.js @@ -427,6 +427,21 @@ describe('LocalDatastore', () => { expect(mockLocalStorageController.fromPinWithName).toHaveBeenCalledTimes(3); }); + it('_serializeObjectsFromPinName null pin', async () => { + const LDS = { + [DEFAULT_PIN]: null, + }; + + mockLocalStorageController + .getAllContents + .mockImplementationOnce(() => LDS); + + const results = await LocalDatastore._serializeObjectsFromPinName(DEFAULT_PIN); + expect(results).toEqual([]); + + expect(mockLocalStorageController.getAllContents).toHaveBeenCalledTimes(1); + }); + it('_serializeObject no children', async () => { const object = new ParseObject('Item'); object.id = 1234; diff --git a/src/__tests__/OfflineQuery-test.js b/src/__tests__/OfflineQuery-test.js index 486de688c..c2e1ca893 100644 --- a/src/__tests__/OfflineQuery-test.js +++ b/src/__tests__/OfflineQuery-test.js @@ -194,6 +194,12 @@ describe('OfflineQuery', () => { json = img.toJSON(); json.owners[0].objectId = 'U3'; expect(matchesQuery(json, q)).toBe(false); + + const u3 = new ParseUser(); + u3.id = 'U3'; + img = new ParseObject('Image'); + img.set('owners', [u3]); + expect(matchesQuery(q.className, img, [], q)).toBe(false); }); it('matches on inequalities', () => { diff --git a/src/__tests__/Parse-test.js b/src/__tests__/Parse-test.js index 4179f168b..97256cdd6 100644 --- a/src/__tests__/Parse-test.js +++ b/src/__tests__/Parse-test.js @@ -9,6 +9,8 @@ jest.dontMock('../CoreManager'); jest.dontMock('../CryptoController'); +jest.dontMock('../decode'); +jest.dontMock('../encode'); jest.dontMock('../Parse'); jest.dontMock('../LocalDatastore'); jest.dontMock('crypto-js/aes'); @@ -46,6 +48,14 @@ describe('Parse module', () => { Parse.masterKey = '789'; expect(CoreManager.get('MASTER_KEY')).toBe('789'); expect(Parse.masterKey).toBe('789'); + + Parse.serverURL = 'http://example.com'; + expect(CoreManager.get('SERVER_URL')).toBe('http://example.com'); + expect(Parse.serverURL).toBe('http://example.com'); + + Parse.liveQueryServerURL = 'https://example.com'; + expect(CoreManager.get('LIVEQUERY_SERVER_URL')).toBe('https://example.com'); + expect(Parse.liveQueryServerURL).toBe('https://example.com'); }); it('can set auth type and token', () => { @@ -142,4 +152,44 @@ describe('Parse module', () => { expect(CoreManager.get('REQUEST_BATCH_SIZE')).toBe(4); CoreManager.set('REQUEST_BATCH_SIZE', 20); }); + + it('_request', () => { + const controller = { + request: jest.fn(), + ajax: jest.fn(), + }; + CoreManager.setRESTController(controller); + Parse._request('POST', 'classes/TestObject'); + const [method, path] = controller.request.mock.calls[0]; + expect(method).toBe('POST'); + expect(path).toBe('classes/TestObject'); + }); + + it('_ajax', () => { + const controller = { + request: jest.fn(), + ajax: jest.fn(), + }; + CoreManager.setRESTController(controller); + Parse._ajax('POST', 'classes/TestObject'); + const [method, path] = controller.ajax.mock.calls[0]; + expect(method).toBe('POST'); + expect(path).toBe('classes/TestObject'); + }); + + it('_getInstallationId', () => { + const controller = { + currentInstallationId: () => '1234', + }; + CoreManager.setInstallationController(controller); + expect(Parse._getInstallationId()).toBe('1234'); + }); + + it('_decode', () => { + expect(Parse._decode(null, 12)).toBe(12); + }); + + it('_encode', () => { + expect(Parse._encode(12)).toBe(12); + }); }); diff --git a/src/__tests__/ParseACL-test.js b/src/__tests__/ParseACL-test.js index 1fa63b356..d9935dd5d 100644 --- a/src/__tests__/ParseACL-test.js +++ b/src/__tests__/ParseACL-test.js @@ -70,6 +70,10 @@ describe('ParseACL', () => { expect(a.setReadAccess.bind(a, 12, true)).toThrow( 'userId must be a string.' ); + + expect(() => { + a.getReadAccess(new ParseUser(), true) + }).toThrow('Cannot get access for a ParseUser without an ID'); }); it('throws when setting an invalid access', () => { @@ -79,6 +83,17 @@ describe('ParseACL', () => { ); }); + it('throws when role does not have name', () => { + const a = new ParseACL(); + expect(() => { + a.setReadAccess(new ParseRole(), true) + }).toThrow('Role must have a name'); + + expect(() => { + a.getReadAccess(new ParseRole(), true) + }).toThrow('Role must have a name'); + }); + it('throws when setting an invalid role', () => { const a = new ParseACL(); expect(a.setRoleReadAccess.bind(a, 12, true)).toThrow( @@ -339,5 +354,13 @@ describe('ParseACL', () => { b.setReadAccess('aUserId', true); expect(a.equals(b)).toBe(false); expect(b.equals(a)).toBe(false); + expect(a.equals({})).toBe(false); + + b.setWriteAccess('newUserId', true); + expect(a.equals(b)).toBe(false); + + a.setPublicReadAccess(false); + a.permissionsById.newUserId = { write: false }; + expect(a.equals(b)).toBe(false); }); }); diff --git a/src/__tests__/ParseConfig-test.js b/src/__tests__/ParseConfig-test.js index 7f863dd9c..a468697d1 100644 --- a/src/__tests__/ParseConfig-test.js +++ b/src/__tests__/ParseConfig-test.js @@ -18,7 +18,9 @@ jest.dontMock('../ParseGeoPoint'); jest.dontMock('../RESTController'); jest.dontMock('../Storage'); jest.dontMock('../StorageController.default'); +jest.dontMock('./test_helpers/mockAsyncStorage'); +const mockAsyncStorage = require('./test_helpers/mockAsyncStorage'); const CoreManager = require('../CoreManager'); const ParseConfig = require('../ParseConfig').default; const ParseGeoPoint = require('../ParseGeoPoint').default; @@ -28,6 +30,10 @@ CoreManager.set('APPLICATION_ID', 'A'); CoreManager.set('JAVASCRIPT_KEY', 'B'); describe('ParseConfig', () => { + beforeEach(() => { + ParseConfig._clearCache(); + }); + it('exposes attributes via get()', () => { const c = new ParseConfig(); c.attributes = { @@ -47,6 +53,7 @@ describe('ParseConfig', () => { }; expect(c.escape('brackets')).toBe('<>'); expect(c.escape('phone')).toBe('AT&T'); + expect(c.escape('phone')).toBe('AT&T'); }); it('can retrieve the current config from disk or cache', () => { @@ -63,6 +70,40 @@ describe('ParseConfig', () => { count: 12, point: new ParseGeoPoint(20.02, 30.03) }); + expect(ParseConfig.current().attributes).toEqual({ + count: 12, + point: new ParseGeoPoint(20.02, 30.03) + }); + }); + + it('can handle decodedData error', async () => { + const currentStorage = CoreManager.getStorageController(); + CoreManager.setStorageController(mockAsyncStorage); + const path = Storage.generatePath('currentConfig'); + await Storage.setItemAsync(path, {}); + const config = await ParseConfig.current(); + expect(config.attributes).toEqual({}); + CoreManager.setStorageController(currentStorage); + }); + + it('can retrieve the current config from async storage', async () => { + const currentStorage = CoreManager.getStorageController(); + CoreManager.setStorageController(mockAsyncStorage); + const path = Storage.generatePath('currentConfig'); + await Storage.setItemAsync(path, JSON.stringify({ + count: 12, + point: { + __type: 'GeoPoint', + latitude: 20.02, + longitude: 30.03 + } + })); + const config = await ParseConfig.current(); + expect(config.attributes).toEqual({ + count: 12, + point: new ParseGeoPoint(20.02, 30.03) + }); + CoreManager.setStorageController(currentStorage); }); it('can get a config object from the network', (done) => { diff --git a/src/__tests__/ParseFile-test.js b/src/__tests__/ParseFile-test.js index 5497c521c..2b03547bf 100644 --- a/src/__tests__/ParseFile-test.js +++ b/src/__tests__/ParseFile-test.js @@ -14,6 +14,8 @@ jest.mock('../ParseACL'); const ParseError = require('../ParseError').default; const ParseFile = require('../ParseFile').default; +const b64Digit = require('../ParseFile').b64Digit; + const ParseObject = require('../ParseObject').default; const CoreManager = require('../CoreManager'); const EventEmitter = require('../EventEmitter'); @@ -133,6 +135,12 @@ describe('ParseFile', () => { }).toThrow('Cannot create a Parse.File with that data.'); }); + it('throws with invalid base64', () => { + expect(function() { + b64Digit(65); + }).toThrow('Tried to encode large digit 65 in base64.'); + }); + it('returns secure url when specified', () => { const file = new ParseFile('parse.txt', { base64: 'ParseA==' }); return file.save().then(function(result) { @@ -531,6 +539,31 @@ describe('FileController', () => { expect(data.contentType).toBe('image/png'); }); + it('download with ajax no response', async () => { + const mockXHR = function () { + return { + DONE: 4, + open: jest.fn(), + send: jest.fn().mockImplementation(function() { + this.response = undefined; + this.readyState = 2; + this.onreadystatechange(); + this.readyState = 4; + this.onreadystatechange(); + }), + getResponseHeader: function() { + return 'image/png'; + } + }; + }; + defaultController._setXHR(mockXHR); + const options = { + requestTask: () => {}, + }; + const data = await defaultController.download('https://example.com/image.png', options); + expect(data).toEqual({}); + }); + it('download with ajax abort', async () => { const mockXHR = function () { return { @@ -832,4 +865,40 @@ describe('FileController', () => { }); expect(handleError).not.toHaveBeenCalled(); }); + + + it('controller saveFile format errors', async () => { + try { + await defaultController.saveFile('parse.txt', { format: 'base64'}); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('saveFile can only be used with File-type sources.'); + } + }); + + it('controller saveBase64 format errors', async () => { + try { + await defaultController.saveBase64('parse.txt', { format: 'file'}); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('saveBase64 can only be used with Base64-type sources.'); + } + }); + + it('controller saveFile file reader errors', async () => { + const fileReader = global.FileReader; + class FileReader { + readAsDataURL() { + this.onerror('Could not load file.'); + } + } + global.FileReader = FileReader; + try { + await defaultController.saveFile('parse.txt', { format: 'file'}); + expect(true).toBe(false); + } catch (e) { + expect(e).toBe('Could not load file.'); + } + global.FileReader = fileReader; + }); }); diff --git a/src/__tests__/ParseGeoPoint-test.js b/src/__tests__/ParseGeoPoint-test.js index b28191d55..70fae22b1 100644 --- a/src/__tests__/ParseGeoPoint-test.js +++ b/src/__tests__/ParseGeoPoint-test.js @@ -10,6 +10,16 @@ jest.autoMockOff(); const ParseGeoPoint = require('../ParseGeoPoint').default; +global.navigator.geolocation = { + getCurrentPosition: (cb) => { + return cb({ + coords: { + latitude: 10, + longitude: 20, + } + }); + } +} describe('GeoPoint', () => { it('can be constructed from various inputs', () => { @@ -214,4 +224,10 @@ describe('GeoPoint', () => { expect(a.equals(b)).toBe(false); expect(b.equals(a)).toBe(false); }); + + it('can get current location', async () => { + const geoPoint = ParseGeoPoint.current(); + expect(geoPoint.latitude).toBe(10); + expect(geoPoint.longitude).toBe(20); + }); }); diff --git a/src/__tests__/ParseInstallation-test.js b/src/__tests__/ParseInstallation-test.js new file mode 100644 index 000000000..ce1b6853d --- /dev/null +++ b/src/__tests__/ParseInstallation-test.js @@ -0,0 +1,28 @@ +jest.dontMock('../CoreManager'); +jest.dontMock('../decode'); +jest.dontMock('../ObjectStateMutations'); +jest.dontMock('../ParseError'); +jest.dontMock('../ParseObject'); +jest.dontMock('../ParseOp'); +jest.dontMock('../ParseInstallation'); +jest.dontMock('../SingleInstanceStateController'); +jest.dontMock('../UniqueInstanceStateController'); + +const ParseInstallation = require('../ParseInstallation').default; + +describe('ParseInstallation', () => { + it('can create ParseInstallation', () => { + let installation = new ParseInstallation(); + expect(installation.className).toBe('_Installation'); + + installation = new ParseInstallation({}); + expect(installation.className).toBe('_Installation'); + + installation = new ParseInstallation({ deviceToken: 'token' }); + expect(installation.get('deviceToken')).toBe('token'); + + expect(() => { + new ParseInstallation({ 'invalid#name' : 'foo'}) + }).toThrow("Can't create an invalid Installation"); + }); +}); diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js index 47c250b5a..25c3cb7c9 100644 --- a/src/__tests__/ParseObject-test.js +++ b/src/__tests__/ParseObject-test.js @@ -180,6 +180,7 @@ function flushPromises() { describe('ParseObject', () => { beforeEach(() => { ParseObject.enableSingleInstance(); + jest.clearAllMocks(); }); it('is initially created with no Id', () => { @@ -198,6 +199,45 @@ describe('ParseObject', () => { expect(o.attributes).toEqual({ value: 12 }); }); + it('can be created with attributes parameter', () => { + const o = new ParseObject('Item', { + value: 12, + }); + expect(o.className).toBe('Item'); + expect(o.attributes).toEqual({ value: 12 }); + }); + + it('can ignore setting invalid key', () => { + const o = new ParseObject('Item'); + const o2 = o.set(1234); + expect(o).toEqual(o2); + }); + + it('can ignore setting createdAt', () => { + const o = new ParseObject('Item'); + o.set('createdAt', '1234'); + expect(o.get('createdAt')).toEqual(undefined); + }); + + it('can handle setting relationOp', () => { + const child = new ParseObject('Child'); + child.id = 'child1234'; + const relationOpJSON = { __op: 'AddRelation', objects: [child] }; + const o = new ParseObject('Item'); + o.set('friends', relationOpJSON); + o._handleSaveResponse({}); + expect(o.get('friends').targetClassName).toBe('Child'); + }); + + it('cannot create with invalid attributes', () => { + expect(() => { + new ParseObject({ + className: 'Item', + 'invalid#name' : 'foo', + }); + }).toThrow("Can't create an invalid Parse Object"); + }); + it('can ignore validation if ignoreValidation option is provided', () => { class ValidatedObject extends ParseObject { validate(attrs) { @@ -285,6 +325,16 @@ describe('ParseObject', () => { expect(o.updatedAt).toEqual(updated); }); + it('fetch ACL from serverData', () => { + const ACL = new ParseACL({ 'user1': { read: true } }); + const o = new ParseObject('Item'); + o._finishFetch({ + objectId: 'O1', + ACL: { 'user1': { read: true } }, + }); + expect(o.getACL()).toEqual(ACL); + }); + it('can be rendered to JSON', () => { let o = new ParseObject('Item'); o.set({ @@ -393,9 +443,11 @@ describe('ParseObject', () => { const o = new ParseObject('Person'); o.set('age', 28); o.set('phoneProvider', 'AT&T'); + o.set('objectField', { toString: 'hacking' }); expect(o.escape('notSet')).toBe(''); expect(o.escape('age')).toBe('28'); expect(o.escape('phoneProvider')).toBe('AT&T'); + expect(o.escape('objectField')).toBe(''); }); it('can tell if it has an attribute', () => { @@ -410,17 +462,21 @@ describe('ParseObject', () => { o._finishFetch({ objectId: 'p99', age: 28, - human: true + human: true, + objectField: { foo: 'bar' }, }); expect(o.dirty()).toBe(false); expect(o.dirty('age')).toBe(false); expect(o.dirty('human')).toBe(false); expect(o.dirty('unset')).toBe(false); + expect(o.dirty('objectField')).toBe(false); o.set('human', false); + o.set('objectField', { foo: 'baz' }); expect(o.dirty()).toBe(true); expect(o.dirty('age')).toBe(false); expect(o.dirty('human')).toBe(true); expect(o.dirty('unset')).toBe(false); + expect(o.dirty('objectField')).toBe(true); }) it('can unset a field', () => { @@ -705,6 +761,17 @@ describe('ParseObject', () => { expect(o2._getSaveJSON().aRelation).toEqual(relationJSON); }); + it('can get relation from relation field', () => { + const relationJSON = {__type: 'Relation', className: 'Bar'}; + const o = ParseObject.fromJSON({ + objectId: '999', + className: 'Foo', + aRelation: relationJSON, + }); + const rel = o.relation('aRelation'); + expect(rel.toJSON()).toEqual(relationJSON); + }); + it('can detect dirty object children', () => { const o = new ParseObject('Person'); o._finishFetch({ @@ -966,6 +1033,7 @@ describe('ParseObject', () => { it('updates the existed flag when saved', () => { const o = new ParseObject('Item'); expect(o.existed()).toBe(false); + expect(o.isNew()).toBe(true); o._handleSaveResponse({ objectId: 'I2' }, 201); @@ -974,6 +1042,12 @@ describe('ParseObject', () => { expect(o.existed()).toBe(true); }); + it('check existed without object state', () => { + const o = new ParseObject('Item'); + o.id = 'test890'; + expect(o.existed()).toBe(false); + }); + it('commits changes to server data when saved', () => { const p = new ParseObject('Person'); p.id = 'P3'; @@ -991,6 +1065,19 @@ describe('ParseObject', () => { expect(p.op('age')).toBe(undefined); }); + it('handle createdAt string for server', () => { + const p = new ParseObject('Person'); + p.id = 'P9'; + const created = new Date(); + p._handleSaveResponse({ + createdAt: created.toISOString() + }); + expect(p._getServerData()).toEqual({ + updatedAt: created, + createdAt: created, + }); + }); + it('isDataAvailable', () => { const p = new ParseObject('Person'); p.id = 'isdataavailable'; @@ -1155,6 +1242,77 @@ describe('ParseObject', () => { spy.mockRestore(); }); + it('fetchAll with empty values', async () => { + CoreManager.getRESTController()._setXHR( + mockXHR([{ + status: 200, + response: [{}] + }]) + ); + const controller = CoreManager.getRESTController(); + jest.spyOn(controller, 'ajax'); + + const results = await ParseObject.fetchAll([]); + expect(results).toEqual([]); + expect(controller.ajax).toHaveBeenCalledTimes(0); + }); + + it('fetchAll unique instance', async () => { + ParseObject.disableSingleInstance(); + const obj = new ParseObject('Item'); + obj.id = 'fetch0'; + const results = await ParseObject.fetchAll([obj]); + expect(results[0].id).toEqual(obj.id); + }); + + it('fetchAll objects does not exist on server', async () => { + jest.spyOn(mockQuery.prototype, 'find').mockImplementationOnce(() => { + return Promise.resolve([]); + }); + const obj = new ParseObject('Item'); + obj.id = 'fetch-1'; + try { + await ParseObject.fetchAll([obj]); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('All objects must exist on the server.'); + } + }); + + it('fetchAll unsaved objects', async () => { + const obj = new ParseObject('Item'); + try { + await ParseObject.fetchAll([obj]); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('All objects must have an ID'); + } + }); + + it('fetchAll objects with different classes', async () => { + const obj = new ParseObject('Item'); + const obj2 = new ParseObject('TestObject'); + try { + await ParseObject.fetchAll([obj, obj2]); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('All objects must have an ID'); + } + }); + + it('fetchAll saved objects with different classes', async () => { + const obj1 = new ParseObject('Item'); + const obj2 = new ParseObject('TestObject'); + obj1.id = 'fetch1'; + obj2.id = 'fetch2'; + try { + await ParseObject.fetchAll([obj1, obj2]); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('All objects should be of the same class'); + } + }); + it('can fetchAllWithInclude', async () => { const objectController = CoreManager.getObjectController(); const spy = jest.spyOn( @@ -1184,11 +1342,60 @@ describe('ParseObject', () => { spy.mockRestore(); }); + it('can fetchAllIfNeededWithInclude', async () => { + const objectController = CoreManager.getObjectController(); + const spy = jest.spyOn( + objectController, + 'fetch' + ) + .mockImplementationOnce(() => {}) + .mockImplementationOnce(() => {}) + .mockImplementationOnce(() => {}); + + const parent = new ParseObject('Person'); + await ParseObject.fetchAllIfNeededWithInclude([parent], 'child', { useMasterKey: true, sessionToken: '123'}); + await ParseObject.fetchAllIfNeededWithInclude([parent], ['child']); + await ParseObject.fetchAllIfNeededWithInclude([parent], [['child']]); + expect(objectController.fetch).toHaveBeenCalledTimes(3); + + expect(objectController.fetch.mock.calls[0]).toEqual([ + [parent], false, { useMasterKey: true, sessionToken: '123', include: ['child'] } + ]); + expect(objectController.fetch.mock.calls[1]).toEqual([ + [parent], false, { include: ['child'] } + ]); + expect(objectController.fetch.mock.calls[2]).toEqual([ + [parent], false, { include: ['child'] } + ]); + + spy.mockRestore(); + }); + it('can check if object exists', async () => { const parent = new ParseObject('Person'); expect(await parent.exists()).toBe(false); parent.id = '1234' expect(await parent.exists()).toBe(true); + + jest.spyOn(mockQuery.prototype, 'get').mockImplementationOnce(() => { + return Promise.reject({ + code: 101, + }); + }); + expect(await parent.exists()).toBe(false); + + jest.spyOn(mockQuery.prototype, 'get').mockImplementationOnce(() => { + return Promise.reject({ + code: 1, + message: 'Internal Server Error', + }); + }); + try { + await parent.exists(); + expect(true).toBe(false); + } catch (e) { + expect(e.code).toBe(1); + } }); it('can save the object', (done) => { @@ -1214,6 +1421,23 @@ describe('ParseObject', () => { }); }); + it('can save the object with key / value', (done) => { + CoreManager.getRESTController()._setXHR( + mockXHR([{ + status: 200, + response: { + objectId: 'P8', + } + }]) + ); + const p = new ParseObject('Person'); + p.save('foo', 'bar').then((obj) => { + expect(obj).toBe(p); + expect(obj.get('foo')).toBe('bar'); + done(); + }); + }); + it('accepts attribute changes on save', (done) => { CoreManager.getRESTController()._setXHR( mockXHR([{ @@ -1509,6 +1733,17 @@ describe('ParseObject', () => { )); }); + it('should fail saveAll batch cycle', async () => { + const obj = new ParseObject('Item'); + obj.set('child', obj); + try { + await ParseObject.saveAll([obj]); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('Tried to save a batch with a cycle.'); + } + }); + it('should fail on invalid date', (done) => { const obj = new ParseObject('Item'); obj.set('when', new Date(Date.parse(null))); @@ -1592,11 +1827,11 @@ describe('ParseObject', () => { const controller = CoreManager.getRESTController(); jest.spyOn(controller, 'ajax'); // Save object - const context = {a: "a"}; + const context = {a: "saveAll"}; const obj = new ParseObject('Item'); obj.id = 'pid'; obj.set('test', 'value'); - await ParseObject.saveAll([obj], {context}) + await ParseObject.saveAll([obj], { context, useMasterKey: true }) // Validate const jsonBody = JSON.parse(controller.ajax.mock.calls[0][2]); expect(jsonBody._context).toEqual(context); @@ -1614,7 +1849,7 @@ describe('ParseObject', () => { const controller = CoreManager.getRESTController(); jest.spyOn(controller, 'ajax'); // Save object - const context = {a: "a"}; + const context = {a: "b"}; const obj = new ParseObject('Item'); obj.id = 'pid'; await ParseObject.destroyAll([obj], { context: context }) @@ -1623,6 +1858,87 @@ describe('ParseObject', () => { expect(jsonBody._context).toEqual(context); }); + it('destroyAll with options', async () => { + // Mock XHR + CoreManager.getRESTController()._setXHR( + mockXHR([{ + status: 200, + response: [{}] + }]) + ); + const controller = CoreManager.getRESTController(); + jest.spyOn(controller, 'ajax'); + + const obj = new ParseObject('Item'); + obj.id = 'pid'; + await ParseObject.destroyAll([obj], { + useMasterKey: true, + sessionToken: 'r:1234', + batchSize: 25, + }); + + const jsonBody = JSON.parse(controller.ajax.mock.calls[0][2]); + expect(jsonBody._MasterKey).toBe('C') + expect(jsonBody._SessionToken).toBe('r:1234'); + }); + + it('destroyAll with empty values', async () => { + CoreManager.getRESTController()._setXHR( + mockXHR([{ + status: 200, + response: [{}] + }]) + ); + const controller = CoreManager.getRESTController(); + jest.spyOn(controller, 'ajax'); + + let results = await ParseObject.destroyAll([]); + expect(results).toEqual([]); + + results = await ParseObject.destroyAll(null); + expect(results).toEqual(null); + expect(controller.ajax).toHaveBeenCalledTimes(0); + }); + + it('destroyAll unsaved objects', async () => { + CoreManager.getRESTController()._setXHR( + mockXHR([{ + status: 200, + response: [{}] + }]) + ); + const controller = CoreManager.getRESTController(); + jest.spyOn(controller, 'ajax'); + + const obj = new ParseObject('Item') + const results = await ParseObject.destroyAll([obj]); + expect(results).toEqual([obj]); + expect(controller.ajax).toHaveBeenCalledTimes(0); + }); + + it('destroyAll handle error response', async () => { + CoreManager.getRESTController()._setXHR( + mockXHR([{ + status: 200, + response: [{ + error: { + code: 101, + error: 'Object not found', + } + }] + }]) + ); + + const obj = new ParseObject('Item') + obj.id = 'toDelete1'; + try { + await ParseObject.destroyAll([obj]); + expect(true).toBe(false); + } catch (e) { + expect(e.code).toBe(600); + } + }); + it('can save a chain of unsaved objects', async () => { const xhrs = []; RESTController._setXHR(function() { @@ -1812,6 +2128,17 @@ describe('ParseObject', () => { expect(jsonBody._context).toEqual(context); }); + it('handle destroy on new object', async () => { + const controller = CoreManager.getRESTController(); + jest.spyOn(controller, 'ajax'); + + const obj = new ParseObject('Item'); + + await obj.destroy({ useMasterKey: true }); + + expect(controller.ajax).toHaveBeenCalledTimes(0); + }); + it('can save an array of objects', async (done) => { const xhr = { setRequestHeader: jest.fn(), @@ -2031,6 +2358,10 @@ describe('ParseObject', () => { }); describe('ObjectController', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('can fetch a single object', async (done) => { const objectController = CoreManager.getObjectController(); const xhr = { @@ -2070,10 +2401,10 @@ describe('ObjectController', () => { const controller = CoreManager.getRESTController(); jest.spyOn(controller, 'ajax'); // Save object - const context = {a: "a"}; + const context = { a: 'fetch' }; const obj = new ParseObject('Item'); obj.id = 'pid'; - await obj.fetch({context}); + await obj.fetch({ context }); // Validate const jsonBody = JSON.parse(controller.ajax.mock.calls[0][2]); expect(jsonBody._context).toEqual(context); @@ -2531,6 +2862,12 @@ describe('ObjectController', () => { expect(o).not.toBe(o2); }); + + it('cannot create a new instance of an object without className', () => { + expect(() => { + ParseObject.fromJSON({}); + }).toThrow('Cannot create an object without a className'); + }); }); describe('ParseObject (unique instance mode)', () => { @@ -2790,6 +3127,20 @@ describe('ParseObject Subclasses', () => { ); }); + it('registerSubclass errors', () => { + expect(() => { + ParseObject.registerSubclass(1234); + }).toThrow('The first argument must be a valid class name.'); + + expect(() => { + ParseObject.registerSubclass('TestObject', undefined); + }).toThrow('You must supply a subclass constructor.'); + + expect(() => { + ParseObject.registerSubclass('TestObject', {}); + }).toThrow('You must register the subclass constructor. Did you attempt to register an instance of the subclass?'); + }); + it('can inflate subclasses from server JSON', () => { const json = { className: 'MyObject', @@ -2818,6 +3169,21 @@ describe('ParseObject Subclasses', () => { expect(o2.id).toBe(undefined); expect(o.equals(o2)).toBe(false); }); + + it('can be cleared', () => { + const o = new MyObject(); + o.set({ + size: 'large', + count: 7, + }); + jest.spyOn(o, 'set'); + o.clear(); + expect(o.set).toHaveBeenCalledWith({ + count: true, size: true, + }, { + unset: true, + }); + }); }); describe('ParseObject extensions', () => { @@ -2884,6 +3250,34 @@ describe('ParseObject extensions', () => { const i = new InitObject() expect(i.get('field')).toBe(12); }); + + it('can handle className parameters', () => { + expect(() => { + ParseObject.extend(); + }).toThrow("Parse.Object.extend's first argument should be the className."); + + let CustomObject = ParseObject.extend('Item'); + expect(CustomObject.className).toBe('Item'); + + CustomObject = ParseObject.extend({ className: 'Test' }); + expect(CustomObject.className).toBe('Test'); + }); + + it('can extend with user rewrite', () => { + const CustomObject = ParseObject.extend('User'); + expect(CustomObject.className).toBe('_User'); + }); + + it('can extend multiple subclasses', () => { + const CustomObject = ParseObject.extend('Item'); + expect(() => { + new CustomObject({ 'invalid#name': 'bar' }); + }).toThrow("Can't create an invalid Parse Object"); + + const CustomUserObject = CustomObject.extend('User'); + const CustomRewrite = CustomUserObject.extend(); + expect(CustomRewrite.className).toBe('_User'); + }); }); describe('ParseObject pin', () => { diff --git a/src/__tests__/ParseOp-test.js b/src/__tests__/ParseOp-test.js index d600dfe4a..bbbf135cf 100644 --- a/src/__tests__/ParseOp-test.js +++ b/src/__tests__/ParseOp-test.js @@ -9,6 +9,7 @@ jest.dontMock('../arrayContainsObject'); jest.dontMock('../encode'); +jest.dontMock('../decode'); jest.dontMock('../ParseOp'); jest.dontMock('../unique'); @@ -23,6 +24,9 @@ const mockObject = function(className, id) { mockObject.prototype._getId = function() { return this.id || this._localId; } +mockObject.fromJSON = function(json) { + return new mockObject(json.className, json.objectId); +} mockObject.registerSubclass = function() {}; jest.setMock('../ParseObject', mockObject); @@ -43,10 +47,24 @@ const { AddOp, AddUniqueOp, RemoveOp, - RelationOp + RelationOp, + opFromJSON, } = ParseOp; describe('ParseOp', () => { + it('base class', () => { + const op = new Op(); + expect(op.applyTo instanceof Function).toBe(true); + expect(op.mergeWith instanceof Function).toBe(true); + expect(op.toJSON instanceof Function).toBe(true); + expect(op.applyTo()).toBeUndefined(); + expect(op.mergeWith()).toBeUndefined(); + expect(op.toJSON()).toBeUndefined(); + expect(opFromJSON({})).toBe(null); + expect(opFromJSON({ __op: 'Unknown' })).toBe(null); + expect(opFromJSON(op.toJSON())).toBe(null); + }); + it('is extended by all Ops', () => { expect(new SetOp(1) instanceof Op).toBe(true); expect(new UnsetOp() instanceof Op).toBe(true); @@ -92,6 +110,7 @@ describe('ParseOp', () => { expect(unset.mergeWith(new RemoveOp(1)) instanceof UnsetOp).toBe(true); expect(unset.toJSON()).toEqual({ __op: 'Delete' }); + expect(opFromJSON(unset.toJSON())).toEqual(unset); }); it('can create and apply Increment Ops', () => { @@ -113,17 +132,28 @@ describe('ParseOp', () => { const bigInc = new IncrementOp(99); expect(bigInc.applyTo(-98)).toBe(1); + expect(bigInc.applyTo()).toBe(99); expect(inc.toJSON()).toEqual({ __op: 'Increment', amount: 1 }); expect(bigInc.toJSON()).toEqual({ __op: 'Increment', amount: 99 }); + expect(opFromJSON(bigInc.toJSON())).toEqual(bigInc); - let merge = inc.mergeWith(new SetOp(11)); + let merge = inc.mergeWith(); + expect(merge instanceof IncrementOp).toBe(true); + expect(merge._amount).toBe(1); + + merge = inc.mergeWith(new SetOp(11)); expect(merge instanceof SetOp).toBe(true); expect(merge._value).toBe(12); + merge = inc.mergeWith(new UnsetOp()); expect(merge instanceof SetOp).toBe(true); expect(merge._value).toBe(1); + merge = inc.mergeWith(bigInc); + expect(merge instanceof IncrementOp).toBe(true); + expect(merge._amount).toBe(100); + expect(inc.mergeWith.bind(inc, new AddOp(1))).toThrow( 'Cannot merge Increment Op with the previous Op' ); @@ -146,6 +176,7 @@ describe('ParseOp', () => { expect(add.applyTo([12])).toEqual([12, 'element']); expect(add.toJSON()).toEqual({ __op: 'Add', objects: ['element'] }); + expect(opFromJSON(add.toJSON())).toEqual(add); const addMany = new AddOp([1, 2, 2, 3, 4, 5]); @@ -153,10 +184,18 @@ describe('ParseOp', () => { expect(addMany.toJSON()).toEqual({ __op: 'Add', objects: [1, 2, 2, 3, 4, 5] }); - let merge = add.mergeWith(new SetOp(['an'])); + let merge = add.mergeWith(null); + expect(merge instanceof AddOp).toBe(true); + expect(merge._value).toEqual(['element']); + + merge = add.mergeWith(new SetOp(['an'])); expect(merge instanceof SetOp).toBe(true); expect(merge._value).toEqual(['an', 'element']); + merge = add.mergeWith(new UnsetOp(['an'])); + expect(merge instanceof SetOp).toBe(true); + expect(merge._value).toEqual(['element']); + merge = add.mergeWith(addMany); expect(merge instanceof AddOp).toBe(true); expect(merge._value).toEqual([1, 2, 2, 3, 4, 5, 'element']); @@ -184,6 +223,7 @@ describe('ParseOp', () => { expect(add.applyTo([12, 'element'])).toEqual([12, 'element']); expect(add.toJSON()).toEqual({ __op: 'AddUnique', objects: ['element'] }); + expect(opFromJSON(add.toJSON())).toEqual(add); const addMany = new AddUniqueOp([1, 2, 2, 3, 4, 5]); @@ -196,10 +236,18 @@ describe('ParseOp', () => { objects: [1, 2, 3, 4, 5] }); - let merge = add.mergeWith(new SetOp(['an', 'element'])); + let merge = add.mergeWith(null); + expect(merge instanceof AddUniqueOp).toBe(true); + expect(merge._value).toEqual(['element']); + + merge = add.mergeWith(new SetOp(['an', 'element'])); expect(merge instanceof SetOp).toBe(true); expect(merge._value).toEqual(['an', 'element']); + merge = add.mergeWith(new UnsetOp(['an'])); + expect(merge instanceof SetOp).toBe(true); + expect(merge._value).toEqual(['element']); + merge = new AddUniqueOp(['an', 'element']) .mergeWith(new AddUniqueOp([1, 2, 'element', 3])); expect(merge instanceof AddUniqueOp).toBe(true); @@ -215,7 +263,7 @@ describe('ParseOp', () => { 'Cannot merge AddUnique Op with the previous Op' ); - const addObjects = new AddUniqueOp(new ParseObject('Item', 'i2')); + let addObjects = new AddUniqueOp(new ParseObject('Item', 'i2')); expect(addObjects.applyTo([ new ParseObject('Item', 'i1'), new ParseObject('Item', 'i2'), @@ -225,6 +273,16 @@ describe('ParseOp', () => { new ParseObject('Item', 'i2'), new ParseObject('Item', 'i3'), ]); + + addObjects = new AddUniqueOp(new ParseObject('Item', 'i2')); + expect(addObjects.applyTo([ + new ParseObject('Item', 'i1'), + new ParseObject('Item', 'i3'), + ])).toEqual([ + new ParseObject('Item', 'i1'), + new ParseObject('Item', 'i3'), + new ParseObject('Item', 'i2'), + ]); }); it('can create and apply Remove Ops', () => { @@ -240,6 +298,7 @@ describe('ParseOp', () => { expect(rem.applyTo(['element', 12, 'element', 'element'])).toEqual([12]); expect(rem.toJSON()).toEqual({ __op: 'Remove', objects: ['element'] }); + expect(opFromJSON(rem.toJSON())).toEqual(rem); const removeMany = new RemoveOp([1, 2, 2, 3, 4, 5]); @@ -250,15 +309,28 @@ describe('ParseOp', () => { objects: [1, 2, 3, 4, 5] }); - let merge = rem.mergeWith(new SetOp(['an', 'element'])); + let merge = rem.mergeWith(null); + expect(merge instanceof RemoveOp).toBe(true); + expect(merge._value).toEqual(['element']); + + merge = rem.mergeWith(new SetOp(['an', 'element'])); expect(merge instanceof SetOp).toBe(true); expect(merge._value).toEqual(['an']); + merge = rem.mergeWith(new UnsetOp(['an'])); + expect(merge instanceof UnsetOp).toBe(true); + expect(merge._value).toEqual(undefined); + merge = new RemoveOp([1, 2, 3]).mergeWith(new RemoveOp([2, 4])); expect(merge instanceof RemoveOp).toBe(true); expect(merge._value).toEqual([2, 4, 1, 3]); + expect(rem.mergeWith.bind(rem, new IncrementOp(1))).toThrow( + 'Cannot merge Remove Op with the previous Op' + ); + const removeObjects = new RemoveOp(new ParseObject('Item', 'i2')); + const previousOp = new RemoveOp(new ParseObject('Item', 'i5')); expect(removeObjects.applyTo([ new ParseObject('Item', 'i1'), new ParseObject('Item', 'i2'), @@ -278,6 +350,11 @@ describe('ParseOp', () => { new ParseObject('Item', 'i1'), new ParseObject('Item', 'i3'), ]); + const merged = removeObjects.mergeWith(previousOp); + expect(merged._value).toEqual([ + new ParseObject('Item', 'i5'), + new ParseObject('Item', 'i2'), + ]); }); it('can create and apply Relation Ops', () => { @@ -320,6 +397,7 @@ describe('ParseOp', () => { { __type: 'Pointer', objectId: 'I2', className: 'Item' } ] }); + expect(opFromJSON(r.toJSON())).toEqual(r); const o3 = new ParseObject('Item'); o3.id = 'I3'; @@ -333,19 +411,31 @@ describe('ParseOp', () => { { __type: 'Pointer', objectId: 'I1', className: 'Item' } ] }); + expect(opFromJSON(r2.toJSON())).toEqual(r2); const rel = r.applyTo(undefined, { className: 'Delivery', id: 'D3' }, 'shipments'); expect(rel.targetClassName).toBe('Item'); expect(r2.applyTo(rel, { className: 'Delivery', id: 'D3' })).toBe(rel); + + const relLocal = r.applyTo(undefined, { className: 'Delivery', id: 'localD4' }, 'shipments'); + expect(relLocal.parent._localId).toBe('localD4'); + expect(r.applyTo.bind(r, 'string')).toThrow( 'Relation cannot be applied to a non-relation field' ); + expect(r.applyTo.bind(r)).toThrow( + 'Cannot apply a RelationOp without either a previous value, or an object and a key' + ); const p = new ParseObject('Person'); p.id = 'P4'; const r3 = new RelationOp([p]); expect(r3.applyTo.bind(r3, rel, { className: 'Delivery', id: 'D3' }, 'packages')) .toThrow('Related object must be a Item, but a Person was passed in.'); + const noRelation = new ParseRelation(null, null); + r3.applyTo(noRelation, { className: 'Delivery', id: 'D3' }, 'packages'); + expect(noRelation.targetClassName).toEqual(r3._targetClassName); + expect(r.mergeWith(null)).toBe(r); expect(r.mergeWith.bind(r, new UnsetOp())).toThrow( 'You cannot modify a relation after deleting it.' @@ -370,13 +460,31 @@ describe('ParseOp', () => { } ] }); + expect(opFromJSON(merged.toJSON())).toEqual(merged); }); it('can merge Relation Op with the previous Op', () => { const r = new RelationOp(); const relation = new ParseRelation(null, null); const set = new SetOp(relation); - expect(r.mergeWith(set)).toEqual(r); + + const a = new ParseObject('Item'); + a.id = 'I1'; + const b = new ParseObject('Item'); + b.id = 'D1'; + const r1 = new RelationOp([a, b], []); + const r2 = new RelationOp([], [b]); + expect(() => { + r.mergeWith(r1) + }).toThrow('Related object must be of class Item, but null was passed in.'); + expect(r1.mergeWith(r2)).toEqual(r1); + expect(r2.mergeWith(r1)).toEqual(new RelationOp([a], [b])); + }); + + it('opFromJSON Relation', () => { + const r = new RelationOp([], []); + expect(opFromJSON({ __op: 'AddRelation', objects: '' })).toEqual(r); + expect(opFromJSON({ __op: 'RemoveRelation', objects: '' })).toEqual(r); }); }); diff --git a/src/__tests__/ParsePolygon-test.js b/src/__tests__/ParsePolygon-test.js new file mode 100644 index 000000000..22b6c29cb --- /dev/null +++ b/src/__tests__/ParsePolygon-test.js @@ -0,0 +1,79 @@ +jest.autoMockOff(); + +const ParseGeoPoint = require('../ParseGeoPoint').default; +const ParsePolygon = require('../ParsePolygon').default; + +const points = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; + +describe('Polygon', () => { + it('can initialize with points', () => { + const polygon = new ParsePolygon(points); + expect(polygon.coordinates).toEqual(points); + }); + + it('can initialize with geopoints', () => { + const geopoints = [ + new ParseGeoPoint(0, 0), + new ParseGeoPoint(0, 1), + new ParseGeoPoint(1, 1), + new ParseGeoPoint(1, 0), + new ParseGeoPoint(0, 0) + ]; + const polygon = new ParsePolygon(geopoints); + expect(polygon.coordinates).toEqual(points); + }); + + it('can set points', () => { + const newPoints = [[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]; + + const polygon = new ParsePolygon(points); + expect(polygon.coordinates).toEqual(points); + + polygon.coordinates = newPoints; + expect(polygon.coordinates).toEqual(newPoints); + }); + + it('toJSON', () => { + const polygon = new ParsePolygon(points); + expect(polygon.toJSON()).toEqual({ + __type: 'Polygon', + coordinates: points, + }); + }); + + it('equals', () => { + const polygon1 = new ParsePolygon(points); + const polygon2 = new ParsePolygon(points); + const geopoint = new ParseGeoPoint(0, 0); + + expect(polygon1.equals(polygon2)).toBe(true); + expect(polygon1.equals(geopoint)).toBe(false); + + const newPoints = [[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]; + polygon1.coordinates = newPoints; + expect(polygon1.equals(polygon2)).toBe(false); + }); + + it('containsPoint', () => { + const polygon = new ParsePolygon(points); + const outside = new ParseGeoPoint(10, 10); + const inside = new ParseGeoPoint(.5, .5); + + expect(polygon.containsPoint(inside)).toBe(true); + expect(polygon.containsPoint(outside)).toBe(false); + }); + + it('throws error on invalid input', () => { + expect(() => { + new ParsePolygon() + }).toThrow('Coordinates must be an Array'); + + expect(() => { + new ParsePolygon([]) + }).toThrow('Polygon must have at least 3 GeoPoints or Points'); + + expect(() => { + new ParsePolygon([1, 2, 3]) + }).toThrow('Coordinates must be an Array of GeoPoints or Points'); + }); +}); diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js index eb8ed6177..84a3ed6a5 100644 --- a/src/__tests__/ParseQuery-test.js +++ b/src/__tests__/ParseQuery-test.js @@ -55,9 +55,21 @@ let ParseObject = require('../ParseObject'); let ParseQuery = require('../ParseQuery').default; const LiveQuerySubscription = require('../LiveQuerySubscription').default; +const MockRESTController = { + request: jest.fn(), + ajax: jest.fn(), +}; + +const QueryController = CoreManager.getQueryController(); + import { DEFAULT_PIN } from '../LocalDatastoreUtils'; describe('ParseQuery', () => { + beforeEach(() => { + CoreManager.setQueryController(QueryController); + CoreManager.setRESTController(MockRESTController); + }); + it('can be constructed from a class name', () => { const q = new ParseQuery('Item'); expect(q.className).toBe('Item'); @@ -75,6 +87,27 @@ describe('ParseQuery', () => { }); }); + it('can be constructed from a function constructor', () => { + function ObjectFunction() { + this.className = 'Item'; + } + const q = new ParseQuery(ObjectFunction); + expect(q.className).toBe('Item'); + expect(q.toJSON()).toEqual({ + where: {} + }); + }); + + it('can be constructed from a function prototype', () => { + function ObjectFunction() {} + ObjectFunction.className = 'Item'; + const q = new ParseQuery(ObjectFunction); + expect(q.className).toBe('Item'); + expect(q.toJSON()).toEqual({ + where: {} + }); + }); + it('throws when created with invalid data', () => { expect(function() { new ParseQuery(); @@ -349,6 +382,17 @@ describe('ParseQuery', () => { } } }); + + q.containsAllStartingWith('tags', 'noArray'); + expect(q.toJSON()).toEqual({ + where: { + tags: { + $all: [ + { $regex: '^\\QnoArray\\E' }, + ] + } + } + }); }); it('can generate exists queries', () => { @@ -884,6 +928,20 @@ describe('ParseQuery', () => { }, order: '-a,-b' }); + + const q2 = new ParseQuery('Item'); + q2.addDescending(); + expect(q2.toJSON()).toEqual({ + where: {}, + order: '', + }); + + const q3 = new ParseQuery('Item'); + q3.addAscending(); + expect(q3.toJSON()).toEqual({ + where: {}, + order: '', + }); }); it('can establish skip counts', () => { @@ -1255,6 +1313,38 @@ describe('ParseQuery', () => { }); }); + it('can handle explain query', (done) => { + CoreManager.setQueryController({ + aggregate() {}, + find(className, params, options) { + expect(className).toBe('Item'); + expect(params).toEqual({ + explain: true, + where: { + size: 'small' + } + }); + expect(options.requestTask).toBeDefined(); + return Promise.resolve({ + results: { + objectId: 'I1', + size: 'small', + name: 'Product 3' + }, + }); + } + }); + + const q = new ParseQuery('Item'); + q.explain(); + q.equalTo('size', 'small').find().then((result) => { + expect(result.objectId).toBe('I1'); + expect(result.size).toBe('small'); + expect(result.name).toEqual('Product 3'); + done(); + }); + }); + it('can get a single object by id', (done) => { CoreManager.setQueryController({ aggregate() {}, @@ -1563,6 +1653,7 @@ describe('ParseQuery', () => { new ParseQuery('Review').equalTo('stars', 5) ); q.equalTo('valid', true); + q.equalTo('arrayField', ['a', 'b']); q.select('size', 'name'); q.includeAll(); q.hint('_id_'); @@ -1579,6 +1670,7 @@ describe('ParseQuery', () => { include: '*', hint: '_id_', where: { + arrayField: ['a', 'b'], size: { $in: ['small', 'medium'] }, @@ -2496,6 +2588,23 @@ describe('ParseQuery', () => { }); }); + it('can issue a query to the controller', () => { + const q = new ParseQuery('Item'); + q.readPreference('PRIMARY', 'SECONDARY', 'SECONDARY_PREFERRED'); + const json = q.toJSON(); + expect(json).toEqual({ + where: {}, + readPreference: 'PRIMARY', + includeReadPreference: 'SECONDARY', + subqueryReadPreference: 'SECONDARY_PREFERRED', + }); + const query = ParseQuery.fromJSON('Item', json); + expect(query._readPreference).toBe('PRIMARY'); + expect(query._includeReadPreference).toBe('SECONDARY'); + expect(query._subqueryReadPreference).toBe('SECONDARY_PREFERRED'); + }); + + it('can issue an aggregate query with array pipeline', (done) => { const pipeline = [ { group: { objectId: '$name' } } @@ -2520,6 +2629,40 @@ describe('ParseQuery', () => { }); }); + it('aggregate query array pipeline with equalTo', (done) => { + const pipeline = [ + { group: { objectId: '$name' } } + ]; + MockRESTController.request.mockImplementationOnce(() => { + return Promise.resolve({ + results: [], + }); + }); + const q = new ParseQuery('Item'); + q.equalTo('name', 'foo'); + q.aggregate(pipeline).then((results) => { + expect(results).toEqual([]); + done(); + }); + }); + + it('aggregate query object pipeline with equalTo', (done) => { + const pipeline = { + group: { objectId: '$name' } + }; + MockRESTController.request.mockImplementationOnce(() => { + return Promise.resolve({ + results: [], + }); + }); + const q = new ParseQuery('Item'); + q.equalTo('name', 'foo'); + q.aggregate(pipeline).then((results) => { + expect(results).toEqual([]); + done(); + }); + }); + it('can issue an aggregate query with object pipeline', (done) => { const pipeline = { group: { objectId: '$name' } @@ -2828,6 +2971,12 @@ describe('ParseQuery', () => { done(); }); + it('full text search invalid option', (done) => { + const query = new ParseQuery('Item'); + expect(() => query.fullText('size', 'medium', { unknown: 'throwOption' })).toThrow('Unknown option: unknown'); + done(); + }); + it('full text search with all parameters', () => { const query = new ParseQuery('Item'); @@ -3118,6 +3267,22 @@ describe('ParseQuery LocalDatastore', () => { expect(results[1].get('number')).toEqual(3); expect(results[2].get('number')).toEqual(2); + q = new ParseQuery('Item'); + q.descending('_created_at'); + q.fromLocalDatastore(); + results = await q.find(); + expect(results[0].get('number')).toEqual(2); + expect(results[1].get('number')).toEqual(3); + expect(results[2].get('number')).toEqual(4); + + q = new ParseQuery('Item'); + q.descending('_updated_at'); + q.fromLocalDatastore(); + results = await q.find(); + expect(results[0].get('number')).toEqual(2); + expect(results[1].get('number')).toEqual(3); + expect(results[2].get('number')).toEqual(4); + q = new ParseQuery('Item'); q.descending('password'); q.fromLocalDatastore(); diff --git a/src/__tests__/ParseRelation-test.js b/src/__tests__/ParseRelation-test.js index c9b3d89e4..7c226eeb1 100644 --- a/src/__tests__/ParseRelation-test.js +++ b/src/__tests__/ParseRelation-test.js @@ -150,6 +150,27 @@ describe('ParseRelation', () => { }); }); + it('cannot add to relation without parent', () => { + const relation = new ParseRelation(); + expect(() => { + relation.add([]); + }).toThrow('Cannot add to a Relation without a parent') + }); + + it('cannot remove from relation without parent', () => { + const relation = new ParseRelation(); + expect(() => { + relation.remove([]); + }).toThrow('Cannot remove from a Relation without a parent') + }); + + it('cannot construct query from relation without parent', () => { + const relation = new ParseRelation(); + expect(() => { + relation.query(); + }).toThrow('Cannot construct a query for a Relation without a parent') + }); + it('can remove objects from a relation', () => { const parent = new ParseObject('Item'); parent.id = 'I2'; @@ -262,8 +283,23 @@ describe('ParseRelation', () => { const r = new ParseRelation(parent, 'shipments'); expect(r._ensureParentAndKey.bind(r, new ParseObject('Item'), 'shipments')) .toThrow('Internal Error. Relation retrieved from two different Objects.'); + expect(() => { + r._ensureParentAndKey(new ParseObject('TestObject'), 'shipments') + }).toThrow('Internal Error. Relation retrieved from two different Objects.') expect(r._ensureParentAndKey.bind(r, parent, 'partners')) .toThrow('Internal Error. Relation retrieved from two different keys.'); expect(r._ensureParentAndKey.bind(r, parent, 'shipments')).not.toThrow(); + + const noParent = new ParseRelation(null, null); + noParent._ensureParentAndKey(parent); + expect(noParent.parent).toEqual(parent); + + const noIdParent = new ParseObject('Item'); + const newParent = new ParseObject('Item'); + newParent.id = 'newId'; + + const hasParent = new ParseRelation(noIdParent); + hasParent._ensureParentAndKey(newParent); + expect(hasParent.parent).toEqual(newParent); }); }); diff --git a/src/__tests__/ParseRole-test.js b/src/__tests__/ParseRole-test.js index 7ccb8e23d..090a544fc 100644 --- a/src/__tests__/ParseRole-test.js +++ b/src/__tests__/ParseRole-test.js @@ -20,6 +20,7 @@ jest.dontMock('../UniqueInstanceStateController'); const ParseACL = require('../ParseACL').default; const ParseError = require('../ParseError').default; const ParseObject = require('../ParseObject').default; +const ParseRelation = require('../ParseRelation').default; const ParseRole = require('../ParseRole').default; describe('ParseRole', () => { @@ -38,6 +39,12 @@ describe('ParseRole', () => { expect(role.getACL()).toBe(acl); }); + it('handle non string name', () => { + const role = new ParseRole(); + role.get = () => 1234; + expect(role.getName()).toBe(''); + }); + it('can validate attributes', () => { const acl = new ParseACL({ aUserId: { read: true, write: true } }); const role = new ParseRole('admin', acl); @@ -68,6 +75,10 @@ describe('ParseRole', () => { expect(role.validate({ name: 'admin' })).toBe(false); + const result = role.validate({ + 'invalid#field': 'admin' + }); + expect(result.code).toBe(ParseError.INVALID_KEY_NAME); }); it('can be constructed from JSON', () => { @@ -80,4 +91,10 @@ describe('ParseRole', () => { expect(role instanceof ParseRole).toBe(true); expect(role.getName()).toBe('admin'); }); + + it('can get relations', () => { + const role = new ParseRole(); + expect(role.getUsers() instanceof ParseRelation).toBe(true); + expect(role.getRoles() instanceof ParseRelation).toBe(true); + }); }); diff --git a/src/__tests__/ParseSession-test.js b/src/__tests__/ParseSession-test.js index b4bcc1ca1..0f278350d 100644 --- a/src/__tests__/ParseSession-test.js +++ b/src/__tests__/ParseSession-test.js @@ -48,6 +48,7 @@ describe('ParseSession', () => { it('can be initialized', () => { let session = new ParseSession(); session.set('someField', 'someValue'); + expect(session.getSessionToken()).toBe(''); expect(session.get('someField')).toBe('someValue'); session = new ParseSession({ @@ -56,6 +57,12 @@ describe('ParseSession', () => { expect(session.get('someField')).toBe('someValue'); }); + it('cannot create schema with invalid fields', () => { + expect(() => { + new ParseSession({ 'invalid#name' : 'foo'}) + }).toThrow("Can't create an invalid Session"); + }); + it('cannot write to readonly fields', () => { const session = new ParseSession(); expect(session.set.bind(session, 'createdWith', 'facebook')).toThrow( @@ -93,6 +100,8 @@ describe('ParseSession', () => { expect(ParseSession.isCurrentSessionRevocable()).toBe(true); mockUser.current = function() { return new mockUser('abc123'); }; expect(ParseSession.isCurrentSessionRevocable()).toBe(false); + mockUser.current = function() { return new mockUser(null); }; + expect(ParseSession.isCurrentSessionRevocable()).toBe(false); }); it('can fetch the full session for the current token', (done) => { @@ -101,7 +110,8 @@ describe('ParseSession', () => { expect(method).toBe('GET'); expect(path).toBe('sessions/me'); expect(options).toEqual({ - sessionToken: 'abc123' + sessionToken: 'abc123', + useMasterKey: true, }); return Promise.resolve({ objectId: 'session1', @@ -114,7 +124,7 @@ describe('ParseSession', () => { mockUser.currentAsync = function() { return Promise.resolve(new mockUser('abc123')); }; - ParseSession.current().then((session) => { + ParseSession.current({ useMasterKey: true }).then((session) => { expect(session instanceof ParseSession).toBe(true); expect(session.id).toBe('session1'); expect(session.getSessionToken()).toBe('abc123'); @@ -122,6 +132,16 @@ describe('ParseSession', () => { }); }); + it('cannot get current session without current user', (done) => { + mockUser.currentAsync = function() { + return Promise.resolve(null); + }; + ParseSession.current().catch((error) => { + expect(error).toBe('There is no current user.') + done(); + }); + }); + it('can be cloned', () => { const s = ParseObject.fromJSON({ className: '_Session', diff --git a/src/__tests__/ParseUser-test.js b/src/__tests__/ParseUser-test.js index b8b93e673..fce28b737 100644 --- a/src/__tests__/ParseUser-test.js +++ b/src/__tests__/ParseUser-test.js @@ -101,6 +101,19 @@ describe('ParseUser', () => { expect(u2.getEmail()).toBe('bono@u2.com'); }); + it('can handle invalid setters and getters', () => { + const u = ParseObject.fromJSON({ + className: '_User', + username: 123, + email: 456, + sessionToken: 789, + }); + expect(u instanceof ParseUser).toBe(true); + expect(u.getUsername()).toBe(''); + expect(u.getEmail()).toBe(''); + expect(u.getSessionToken()).toBe(''); + }); + it('can clone User objects', () => { const u = ParseObject.fromJSON({ className: '_User', @@ -229,6 +242,32 @@ describe('ParseUser', () => { }); }); + it('can log in as a user with options', async () => { + ParseUser.enableUnsafeCurrentUser(); + ParseUser._clearCache(); + CoreManager.setRESTController({ + request(method, path, body, options) { + expect(method).toBe('GET'); + expect(path).toBe('login'); + expect(body.username).toBe('username'); + expect(body.password).toBe('password'); + expect(options.useMasterKey).toBe(true); + expect(options.installationId).toBe('installation1234') + return Promise.resolve({ + objectId: 'uid2', + username: 'username', + sessionToken: '123abc' + }, 200); + }, + ajax() {} + }); + const user = await ParseUser.logIn('username', 'password', { + useMasterKey: true, + installationId: 'installation1234', + }); + expect(user.id).toBe('uid2'); + }); + it('can log in as a user with POST method', (done) => { ParseUser.enableUnsafeCurrentUser(); ParseUser._clearCache(); @@ -345,7 +384,7 @@ describe('ParseUser', () => { expect(method).toBe('GET'); expect(path).toBe('users/me'); expect(options.sessionToken).toBe('123abc'); - + expect(options.useMasterKey).toBe(true); return Promise.resolve({ objectId: 'uid3', username: 'username', @@ -355,13 +394,25 @@ describe('ParseUser', () => { ajax() {} }); - const u = await ParseUser.become('123abc'); + const u = await ParseUser.become('123abc', { useMasterKey: true }); expect(u.id).toBe('uid3'); expect(u.isCurrent()).toBe(true); expect(u.existed()).toBe(true); CoreManager.setStorageController(currentStorage); }); + it('cannot get synchronous current user with async storage', async () => { + const StorageController = CoreManager.getStorageController(); + CoreManager.setStorageController(mockAsyncStorage); + ParseUser.enableUnsafeCurrentUser(); + ParseUser._clearCache(); + expect(() => { + ParseUser.current(); + }).toThrow('Cannot call currentUser() when using a platform with an async storage system. Call currentUserAsync() instead.'); + + CoreManager.setStorageController(StorageController); + }); + it('can hydrate a user with sessionToken in server environment', async () => { ParseUser.enableUnsafeCurrentUser(); @@ -415,6 +466,19 @@ describe('ParseUser', () => { }); ParseUser.requestPasswordReset('me@parse.com'); + + CoreManager.setRESTController({ + request(method, path, body, options) { + expect(method).toBe('POST'); + expect(path).toBe('requestPasswordReset'); + expect(body).toEqual({ email: 'me@parse.com' }); + expect(options.useMasterKey).toBe(true); + return Promise.resolve({}, 200); + }, + ajax() {} + }); + + ParseUser.requestPasswordReset('me@parse.com', { useMasterKey: true }); }); it('can log out a user', (done) => { @@ -505,6 +569,11 @@ describe('ParseUser', () => { expect(u instanceof ParseUser).toBe(true); expect(u.getUsername()).toBe('username'); expect(u.id).toBe('uid6'); + + ParseUser.disableUnsafeCurrentUser(); + return ParseUser.currentAsync(); + }).then((u) => { + expect(u).toBe(null); done(); }); }); @@ -526,6 +595,41 @@ describe('ParseUser', () => { expect(u.getUsername()).toBe('bob'); expect(u.id).toBe('abc'); expect(u.getSessionToken()).toBe('12345'); + + ParseUser._clearCache(); + const user = ParseUser.current(); + expect(user instanceof ParseUser).toBe(true); + expect(user.getUsername()).toBe('bob'); + expect(user.id).toBe('abc'); + expect(user.getSessionToken()).toBe('12345'); + done(); + }); + }); + + it('can inflate users stored from previous SDK versions override _id', (done) => { + ParseUser.enableUnsafeCurrentUser(); + ParseUser._clearCache(); + Storage._clear(); + const path = Storage.generatePath('currentUser'); + Storage.setItem(path, JSON.stringify({ + _id: 'abc', + _sessionToken: '12345', + objectId: 'SET', + username: 'bob', + count: 12 + })); + ParseUser.currentAsync().then((u) => { + expect(u instanceof ParseUser).toBe(true); + expect(u.getUsername()).toBe('bob'); + expect(u.id).toBe('abc'); + expect(u.getSessionToken()).toBe('12345'); + + ParseUser._clearCache(); + const user = ParseUser.current(); + expect(user instanceof ParseUser).toBe(true); + expect(user.getUsername()).toBe('bob'); + expect(user.id).toBe('abc'); + expect(user.getSessionToken()).toBe('12345'); done(); }); }); @@ -1052,6 +1156,31 @@ describe('ParseUser', () => { expect(user.get('authData')).toEqual({ test: { id: 'id', access_token: 'access_token' } }); }); + it('handle linkWith authentication failure', async () => { + const provider = { + authenticate(options) { + if (options.error) { + options.error(this, { + message: 'authentication failed', + }); + } + }, + restoreAuthentication() {}, + getAuthType() { + return 'test'; + }, + deauthenticate() {} + }; + + const user = new ParseUser(); + try { + await user.linkWith(provider, null); + expect(false).toBe(true); + } catch (e) { + expect(e.message).toBe('authentication failed') + } + }); + it('can linkWith if no provider', async () => { ParseUser._clearCache(); CoreManager.setRESTController({ @@ -1079,6 +1208,123 @@ describe('ParseUser', () => { expect(authProvider).toBe('testProvider'); }); + it('cannot linkWith invalid authData', async () => { + ParseUser._clearCache(); + const user = new ParseUser(); + user.set('authData', 1234); + try { + await user.linkWith('testProvider', { authData: { id: 'test' } }); + expect(false).toBe(true); + } catch (e) { + expect(e.message).toBe('Invalid type: authData field should be an object'); + } + }); + + it('_synchronizeAuthData can unlink on failure to restore auth ', async () => { + ParseUser.enableUnsafeCurrentUser(); + ParseUser._clearCache(); + + const provider = { + restoreAuthentication() { + return false; + }, + getAuthType() { + return 'test'; + }, + }; + + const user = new ParseUser(); + user.id = 'sync123'; + user.set('authData', { test: true }); + + ParseUser._setCurrentUserCache(user); + jest.spyOn(user, '_unlinkFrom'); + user._synchronizeAuthData(provider); + expect(user._unlinkFrom).toHaveBeenCalledTimes(1); + }); + + it('_isLinked', () => { + const user = new ParseUser(); + const provider = { + getAuthType: () => 'customAuth', + }; + + user.set('authData', { 'customAuth': true }); + expect(user._isLinked(provider)).toBe(true); + + user.set('authData', 1234); + expect(user._isLinked(provider)).toBe(false); + }); + + it('_cleanupAuthData', () => { + ParseUser.enableUnsafeCurrentUser(); + const user = new ParseUser(); + user.id = 'cleanupData1'; + user.set('authData', { toRemove: null, test: true }); + user._cleanupAuthData(); + expect(user.get('authData')).toEqual({ toRemove: null, test: true }); + + ParseUser._setCurrentUserCache(user); + user._cleanupAuthData(); + expect(user.get('authData')).toEqual({ test: true }); + + user.set('authData', 1234); + user._cleanupAuthData(); + expect(user.get('authData')).toEqual(1234); + }); + + it('_logOutWith', () => { + const user = new ParseUser(); + user.id = 'logout1234'; + const provider = { + deauthenticate: jest.fn(), + }; + user._logOutWith(provider); + expect(provider.deauthenticate).toHaveBeenCalledTimes(0); + + ParseUser._setCurrentUserCache(user); + user._logOutWith(provider); + expect(provider.deauthenticate).toHaveBeenCalledTimes(1); + }); + + it('_logInWith', async () => { + ParseUser.disableUnsafeCurrentUser(); + ParseUser._clearCache(); + CoreManager.setRESTController({ + request() { + return Promise.resolve({ + objectId: 'uid10', + sessionToken: 'r:123abc', + authData: { + test: { + id: 'id', + access_token: 'access_token' + } + } + }, 200); + }, + ajax() {} + }); + const provider = { + authenticate(options) { + if (options.success) { + options.success(this, { + id: 'id', + access_token: 'access_token' + }); + } + }, + restoreAuthentication() {}, + getAuthType() { + return 'test'; + }, + deauthenticate() {} + }; + + const user = await ParseUser._logInWith(provider, null, { useMasterKey: true }); + expect(user.get('authData')).toEqual({ test: { id: 'id', access_token: 'access_token' } }); + }); + it('can encrypt user', async () => { CoreManager.set('ENCRYPTED_USER', true); CoreManager.set('ENCRYPTED_KEY', 'hello'); @@ -1193,6 +1439,7 @@ describe('ParseUser', () => { expect(method).toBe('POST'); expect(path).toBe('users'); expect(options.installationId).toBe(installationId); + expect(options.useMasterKey).toBe(true); return Promise.resolve({ objectId: 'uid3', username: 'username', @@ -1202,7 +1449,7 @@ describe('ParseUser', () => { ajax() {} }); - const user = await ParseUser.signUp('username', 'password', null, { installationId }); + const user = await ParseUser.signUp('username', 'password', null, { installationId, useMasterKey: true }); expect(user.id).toBe('uid3'); expect(user.isCurrent()).toBe(false); expect(user.existed()).toBe(true); @@ -1252,6 +1499,12 @@ describe('ParseUser', () => { expect(user.objectId).toBe('uid2'); expect(user.username).toBe('username'); + const notStatic = new ParseUser(); + notStatic.setUsername('username'); + const userAgain = await notStatic.verifyPassword('password', { useMasterKey: true }); + expect(userAgain.objectId).toBe('uid2'); + expect(userAgain.username).toBe('username'); + CoreManager.setRESTController({ request() { const parseError = new ParseError( @@ -1269,6 +1522,19 @@ describe('ParseUser', () => { expect(error.code).toBe(101); expect(error.message).toBe('Invalid username/password.'); } + try { + await ParseUser.verifyPassword(null, 'password'); + } catch(error) { + expect(error.code).toBe(-1); + expect(error.message).toBe('Username must be a string.'); + } + + try { + await ParseUser.verifyPassword('username', null); + } catch(error) { + expect(error.code).toBe(-1); + expect(error.message).toBe('Password must be a string.'); + } }); it('can send an email verification request', () => { @@ -1284,5 +1550,84 @@ describe('ParseUser', () => { }); ParseUser.requestEmailVerification("me@parse.com"); + + CoreManager.setRESTController({ + request(method, path, body, options) { + expect(method).toBe('POST'); + expect(path).toBe("verificationEmailRequest"); + expect(body).toEqual({ email: "me@parse.com" }); + expect(options.useMasterKey).toBe(true); + return Promise.resolve({}, 200); + }, + ajax() {} + }); + ParseUser.requestEmailVerification("me@parse.com", { useMasterKey: true }); + }); + + it('allowCustomUserClass', () => { + expect(CoreManager.get('PERFORM_USER_REWRITE')).toBe(true); + ParseUser.allowCustomUserClass(true); + expect(CoreManager.get('PERFORM_USER_REWRITE')).toBe(false); + ParseUser.allowCustomUserClass(false); + expect(CoreManager.get('PERFORM_USER_REWRITE')).toBe(true); + }); + + it('enableRevocableSession', async () => { + const result = await ParseUser.enableRevocableSession(); + expect(CoreManager.get('FORCE_REVOCABLE_SESSION')).toBe(true); + expect(result).toBeUndefined(); + + ParseUser.enableUnsafeCurrentUser(); + ParseUser._clearCache(); + CoreManager.setRESTController({ + request() { + return Promise.resolve({ + objectId: 'uid2', + username: 'username', + sessionToken: '123abc' + }, 200); + }, + ajax() {} + }); + const user = await ParseUser.logIn('username', 'password'); + jest.spyOn(user, '_upgradeToRevocableSession'); + await ParseUser.enableRevocableSession({ useMasterKey: true }); + expect(user._upgradeToRevocableSession).toHaveBeenCalled(); + }); + + it('upgradeToRevocableSession', async () => { + try { + const unsavedUser = new ParseUser(); + await unsavedUser._upgradeToRevocableSession(); + } catch (e) { + expect(e.message).toBe('Cannot upgrade a user with no session token'); + } + + ParseUser.disableUnsafeCurrentUser(); + ParseUser._clearCache(); + CoreManager.setRESTController({ + request() { + return Promise.resolve({ + objectId: 'uid2', + username: 'username', + sessionToken: '123abc' + }, 200); + }, + ajax() {} + }); + const user = await ParseUser.logIn('username', 'password'); + const upgradedUser = await user._upgradeToRevocableSession(); + expect(user).toEqual(upgradedUser); + }); + + it('extend', () => { + let CustomUser = ParseUser.extend(); + expect(CustomUser instanceof ParseUser); + + CustomUser = ParseUser.extend({ test: true, className: 'Item' }, { test: false, className: 'Item' }); + expect(CustomUser instanceof ParseUser); + + const user = new CustomUser(); + expect(user.test).toBe(true); }); }); diff --git a/src/__tests__/RESTController-test.js b/src/__tests__/RESTController-test.js index 73b58b60d..80cdf1040 100644 --- a/src/__tests__/RESTController-test.js +++ b/src/__tests__/RESTController-test.js @@ -17,6 +17,9 @@ jest.mock('uuid/v4', () => { const CoreManager = require('../CoreManager'); const RESTController = require('../RESTController'); const mockXHR = require('./test_helpers/mockXHR'); +const mockWeChat = require('./test_helpers/mockWeChat'); + +global.wx = mockWeChat; CoreManager.setInstallationController({ currentInstallationId() { @@ -558,4 +561,68 @@ describe('RESTController', () => { expect(xhr.send.mock.calls[0][0]).toEqual({}); CoreManager.set('REQUEST_HEADERS', {}); }); + + it('can handle installationId option', async () => { + const xhr = { + setRequestHeader: jest.fn(), + open: jest.fn(), + send: jest.fn() + }; + RESTController._setXHR(function() { return xhr; }); + RESTController.request('GET', 'classes/MyObject', {}, { sessionToken: '1234', installationId: '5678' }); + await flushPromises(); + expect(xhr.open.mock.calls[0]).toEqual( + ['POST', 'https://api.parse.com/1/classes/MyObject', true] + ); + expect(JSON.parse(xhr.send.mock.calls[0][0])).toEqual({ + _method: 'GET', + _ApplicationId: 'A', + _JavaScriptKey: 'B', + _ClientVersion: 'V', + _InstallationId: '5678', + _SessionToken: '1234', + }); + }); + + it('can handle wechat request', async () => { + const XHR = require('../Xhr.weapp'); + const xhr = new XHR(); + jest.spyOn(xhr, 'open'); + jest.spyOn(xhr, 'send'); + RESTController._setXHR(function() { return xhr; }); + RESTController.request('GET', 'classes/MyObject', {}, { sessionToken: '1234', installationId: '5678' }); + await flushPromises(); + expect(xhr.open.mock.calls[0]).toEqual( + ['POST', 'https://api.parse.com/1/classes/MyObject', true] + ); + expect(JSON.parse(xhr.send.mock.calls[0][0])).toEqual({ + _method: 'GET', + _ApplicationId: 'A', + _JavaScriptKey: 'B', + _ClientVersion: 'V', + _InstallationId: '5678', + _SessionToken: '1234', + }); + }); + + it('can handle wechat ajax', async () => { + const XHR = require('../Xhr.weapp'); + const xhr = new XHR(); + jest.spyOn(xhr, 'open'); + jest.spyOn(xhr, 'send'); + jest.spyOn(xhr, 'setRequestHeader'); + RESTController._setXHR(function() { return xhr; }); + const headers = { 'X-Parse-Session-Token': '123' }; + RESTController.ajax('GET', 'users/me', {}, headers); + expect(xhr.setRequestHeader.mock.calls[0]).toEqual( + [ 'X-Parse-Session-Token', '123' ] + ); + expect(xhr.open.mock.calls[0]).toEqual([ 'GET', 'users/me', true ]); + expect(xhr.send.mock.calls[0][0]).toEqual({}); + xhr.responseHeader = headers; + expect(xhr.getAllResponseHeaders().includes('X-Parse-Session-Token')).toBe(true); + expect(xhr.getResponseHeader('X-Parse-Session-Token')).toBe('123'); + xhr.abort(); + xhr.abort(); + }); }); diff --git a/src/__tests__/SingleInstanceStateController-test.js b/src/__tests__/SingleInstanceStateController-test.js index 0384b87fe..4bad43381 100644 --- a/src/__tests__/SingleInstanceStateController-test.js +++ b/src/__tests__/SingleInstanceStateController-test.js @@ -385,4 +385,23 @@ describe('SingleInstanceStateController', () => { }); expect(SingleInstanceStateController.getState({ className: 'someClass', id: 'P' })).toBe(null); }); + + it('can duplicate the state of an object', () => { + const obj = { className: 'someClass', id: 'someId' }; + SingleInstanceStateController.setServerData(obj, { counter: 12, name: 'original' }); + const setCount = new ParseOps.SetOp(44); + const setValid = new ParseOps.SetOp(true); + SingleInstanceStateController.setPendingOp(obj, 'counter', setCount); + SingleInstanceStateController.setPendingOp(obj, 'valid', setValid); + + const duplicate = { className: 'someClass', id: 'dupId' }; + SingleInstanceStateController.duplicateState(obj, duplicate); + expect(SingleInstanceStateController.getState(duplicate)).toEqual({ + serverData: { counter: 12, name: 'original' }, + pendingOps: [{ counter: setCount, valid: setValid }], + objectCache: {}, + tasks: new TaskQueue(), + existed: false + }); + }); }); diff --git a/src/__tests__/Storage-test.js b/src/__tests__/Storage-test.js index 96ab1b43f..749a6d58a 100644 --- a/src/__tests__/Storage-test.js +++ b/src/__tests__/Storage-test.js @@ -10,8 +10,11 @@ jest.autoMockOff(); const mockRNStorageInterface = require('./test_helpers/mockRNStorage'); +const mockWeChat = require('./test_helpers/mockWeChat'); const CoreManager = require('../CoreManager'); +global.wx = mockWeChat; + let mockStorage = {}; const mockStorageInterface = { getItem(path) { @@ -207,6 +210,35 @@ describe('Default StorageController', () => { }); }); +const WeappStorageController = require('../StorageController.weapp'); + +describe('WeChat StorageController', () => { + beforeEach(() => { + WeappStorageController.clear(); + }); + + it('is synchronous', () => { + expect(WeappStorageController.async).toBe(0); + expect(typeof WeappStorageController.getItem).toBe('function'); + expect(typeof WeappStorageController.setItem).toBe('function'); + expect(typeof WeappStorageController.removeItem).toBe('function'); + }); + + it('can store and retrieve values', () => { + expect(WeappStorageController.getItem('myKey')).toBe(undefined); + WeappStorageController.setItem('myKey', 'myValue'); + expect(WeappStorageController.getItem('myKey')).toBe('myValue'); + expect(WeappStorageController.getAllKeys()).toEqual(['myKey']); + }); + + it('can remove values', () => { + WeappStorageController.setItem('myKey', 'myValue'); + expect(WeappStorageController.getItem('myKey')).toBe('myValue'); + WeappStorageController.removeItem('myKey'); + expect(WeappStorageController.getItem('myKey')).toBe(undefined); + }); +}); + const Storage = require('../Storage'); describe('Storage (Default StorageController)', () => { @@ -255,6 +287,16 @@ describe('Storage (Default StorageController)', () => { expect(Storage.generatePath('hello')).toBe('Parse/appid/hello'); expect(Storage.generatePath('/hello')).toBe('Parse/appid/hello'); }); + + it('can clear if controller does not implement clear', () => { + CoreManager.setStorageController({ + getItem: () => {}, + setItem: () => {}, + removeItem: () => {}, + getAllKeys: () => {}, + }); + Storage._clear(); + }); }); describe('Storage (Async StorageController)', () => { diff --git a/src/__tests__/TaskQueue-test.js b/src/__tests__/TaskQueue-test.js index 0b9a34a64..f902bae8a 100644 --- a/src/__tests__/TaskQueue-test.js +++ b/src/__tests__/TaskQueue-test.js @@ -127,4 +127,43 @@ describe('TaskQueue', () => { await new Promise(r => setImmediate(r)); expect(q.queue.length).toBe(0); }); + + it('continues the chain when all tasks errors', async () => { + const q = new TaskQueue(); + const called = [false, false, false]; + const promises = [resolvingPromise(), resolvingPromise(), resolvingPromise()]; + q.enqueue(() => { + called[0] = true; + return promises[0]; + }).catch(() => {}); + + q.enqueue(() => { + called[1] = true; + return promises[1]; + }).catch(() => {}); + + q.enqueue(() => { + called[2] = true; + return promises[2]; + }).catch(() => {}); + + expect(called).toEqual([true, false, false]); + + promises[0].catch(() => {}); + promises[0].reject('1 oops'); + await new Promise(r => setImmediate(r)); + expect(called).toEqual([true, true, false]); + expect(q.queue.length).toBe(2); + + promises[1].catch(() => {}); + promises[1].reject('2 oops'); + await new Promise(r => setImmediate(r)); + expect(called).toEqual([true, true, true]); + expect(q.queue.length).toBe(1); + + promises[2].catch(() => {}); + promises[2].reject('3 oops'); + await new Promise(r => setImmediate(r)); + expect(q.queue.length).toBe(0); + }); }); diff --git a/src/__tests__/browser-test.js b/src/__tests__/browser-test.js new file mode 100644 index 000000000..3fe964f99 --- /dev/null +++ b/src/__tests__/browser-test.js @@ -0,0 +1,134 @@ +jest.dontMock('../CoreManager'); +jest.dontMock('../CryptoController'); +jest.dontMock('../decode'); +jest.dontMock('../encode'); +jest.dontMock('../ParseError'); +jest.dontMock('../EventEmitter'); +jest.dontMock('../Parse'); +jest.dontMock('../RESTController'); +jest.dontMock('../Storage'); +jest.dontMock('crypto-js/aes'); + +const ParseError = require('../ParseError').default; + +class XMLHttpRequest {} +class XDomainRequest { + open() {} + send() {} +} +global.XMLHttpRequest = XMLHttpRequest; +global.XDomainRequest = XDomainRequest; + +describe('Browser', () => { + beforeEach(() => { + process.env.PARSE_BUILD = 'browser'; + jest.clearAllMocks(); + }); + + afterEach(() => { + process.env.PARSE_BUILD = 'node'; + }); + + it('warning initializing parse/node in browser', () => { + const Parse = require('../Parse'); + jest.spyOn(console, 'log').mockImplementationOnce(() => {}); + jest.spyOn(Parse, '_initialize').mockImplementationOnce(() => {}); + Parse.initialize('A', 'B'); + expect(console.log).toHaveBeenCalledWith("It looks like you're using the browser version of the SDK in a node.js environment. You should require('parse/node') instead."); + expect(Parse._initialize).toHaveBeenCalledTimes(1); + }); + + it('initializing parse/node in browser with server rendering', () => { + process.env.SERVER_RENDERING = true; + const Parse = require('../Parse'); + jest.spyOn(console, 'log').mockImplementationOnce(() => {}); + jest.spyOn(Parse, '_initialize').mockImplementationOnce(() => {}); + Parse.initialize('A', 'B'); + expect(console.log).toHaveBeenCalledTimes(0); + expect(Parse._initialize).toHaveBeenCalledTimes(1); + }); + + it('load StorageController', () => { + const StorageController = require('../StorageController.browser'); + jest.spyOn(StorageController, 'setItem'); + const storage = require('../Storage'); + storage.setItem('key', 'value'); + expect(StorageController.setItem).toHaveBeenCalledTimes(1); + }); + + it('load RESTController with IE9', async () => { + let called = false; + class XDomainRequest { + open() { + called = true; + } + send() { + this.responseText = JSON.stringify({ status: 200 }); + this.onprogress(); + this.onload(); + } + } + global.XDomainRequest = XDomainRequest; + console.log('hererer'); + const RESTController = require('../RESTController'); + const options = { + progress: () => {}, + requestTask: () => {}, + }; + const { response } = await RESTController.ajax('POST', 'classes/TestObject', null, null, options); + expect(response.status).toBe(200); + expect(called).toBe(true); + }); + + it('RESTController IE9 Ajax timeout error', async () => { + let called = false; + class XDomainRequest { + open() { + called = true; + } + send() { + this.responseText = ''; + this.ontimeout(); + } + } + class XMLHttpRequest {} + global.XDomainRequest = XDomainRequest; + global.XMLHttpRequest = XMLHttpRequest; + const RESTController = require('../RESTController'); + try { + await RESTController.ajax('POST', 'classes/TestObject'); + expect(true).toBe(false); + } catch (e) { + const errorResponse = JSON.stringify({ + code: ParseError.X_DOMAIN_REQUEST, + error: 'IE\'s XDomainRequest does not supply error info.' + }); + expect(e.responseText).toEqual(errorResponse); + } + expect(called).toBe(true); + }); + + it('RESTController IE9 Ajax response error', async () => { + let called = false; + class XDomainRequest { + open() { + called = true; + } + send() { + this.responseText = ''; + this.onload(); + } + } + class XMLHttpRequest {} + global.XDomainRequest = XDomainRequest; + global.XMLHttpRequest = XMLHttpRequest; + const RESTController = require('../RESTController'); + try { + await RESTController.ajax('POST', 'classes/TestObject'); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('Unexpected end of JSON input'); + } + expect(called).toBe(true); + }); +}); diff --git a/src/__tests__/canBeSerialized-test.js b/src/__tests__/canBeSerialized-test.js index 954e32595..45a1c14da 100644 --- a/src/__tests__/canBeSerialized-test.js +++ b/src/__tests__/canBeSerialized-test.js @@ -27,6 +27,7 @@ jest.setMock('../ParseFile', mockFile); const canBeSerialized = require('../canBeSerialized').default; const ParseFile = require('../ParseFile'); const ParseObject = require('../ParseObject'); +const ParseRelation = require('../ParseRelation').default; describe('canBeSerialized', () => { it('returns true for anything that is not a ParseObject', () => { @@ -78,6 +79,14 @@ describe('canBeSerialized', () => { expect(canBeSerialized(child)).toBe(false); }); + it('returns true for relations', () => { + const relation = new ParseRelation(null, null); + const parent = new ParseObject(undefined, { + child: relation, + }); + expect(canBeSerialized(parent)).toBe(true); + }); + it('traverses nested arrays and objects', () => { let o = new ParseObject('oid', { a: { diff --git a/src/__tests__/decode-test.js b/src/__tests__/decode-test.js index 2397ccacf..0e6b4cfdd 100644 --- a/src/__tests__/decode-test.js +++ b/src/__tests__/decode-test.js @@ -10,12 +10,14 @@ jest.dontMock('../decode'); jest.dontMock('../ParseFile'); jest.dontMock('../ParseGeoPoint'); +jest.dontMock('../ParsePolygon'); const decode = require('../decode').default; const ParseFile = require('../ParseFile').default; const ParseGeoPoint = require('../ParseGeoPoint').default; const ParseObject = require('../ParseObject').default; +const ParsePolygon = require('../ParsePolygon').default; describe('decode', () => { it('ignores primitives', () => { @@ -44,6 +46,16 @@ describe('decode', () => { expect(point.longitude).toBe(50.4); }); + it('decodes Polygons', () => { + const points = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; + const polygon = decode({ + __type: 'Polygon', + coordinates: points, + }); + expect(polygon instanceof ParsePolygon).toBe(true); + expect(polygon.coordinates).toEqual(points); + }); + it('decodes Files', () => { const file = decode({ __type: 'File', diff --git a/src/__tests__/promiseUtils-test.js b/src/__tests__/promiseUtils-test.js new file mode 100644 index 000000000..41ff17ded --- /dev/null +++ b/src/__tests__/promiseUtils-test.js @@ -0,0 +1,19 @@ +jest.autoMockOff(); + +const { when } = require('../promiseUtils'); + +describe('promiseUtils', () => { + it('when', async () => { + const promise1 = Promise.resolve(1); + const promise2 = Promise.resolve(2); + + let result = await when([]); + expect(result).toEqual([[]]); + + result = await when(promise1, promise2); + expect(result).toEqual([1, 2]); + + result = await when(promise1, 'not a promise'); + expect(result).toEqual([1, 'not a promise']); + }); +}); diff --git a/src/__tests__/react-native-test.js b/src/__tests__/react-native-test.js new file mode 100644 index 000000000..3c5452bae --- /dev/null +++ b/src/__tests__/react-native-test.js @@ -0,0 +1,74 @@ +/* global WebSocket */ +jest.dontMock('../CoreManager'); +jest.dontMock('../CryptoController'); +jest.dontMock('../decode'); +jest.dontMock('../encode'); +jest.dontMock('../EventEmitter'); +jest.dontMock('../LiveQueryClient'); +jest.dontMock('../LocalDatastore'); +jest.dontMock('../ParseObject'); +jest.dontMock('../Storage'); + +jest.mock('../../../../react-native/Libraries/vendor/emitter/EventEmitter', () => { + return { + prototype: { + addListener: new (require('events').EventEmitter)(), + }, + }; +}, { virtual: true }); + +const mockEmitter = require('../../../../react-native/Libraries/vendor/emitter/EventEmitter'); +const CoreManager = require('../CoreManager'); + +describe('React Native', () => { + beforeEach(() => { + process.env.PARSE_BUILD = 'react-native'; + }); + + afterEach(() => { + process.env.PARSE_BUILD = 'node'; + }); + + it('load EventEmitter', () => { + const eventEmitter = require('../EventEmitter'); + expect(eventEmitter).toEqual(mockEmitter); + }); + + it('load CryptoController', () => { + const CryptoJS = require('react-native-crypto-js'); + jest.spyOn(CryptoJS.AES, 'encrypt').mockImplementation(() => { + return { + toString: () => 'World', + }; + }); + const CryptoController = require('../CryptoController'); + const phrase = CryptoController.encrypt({}, 'salt'); + expect(phrase).toBe('World'); + expect(CryptoJS.AES.encrypt).toHaveBeenCalled(); + }); + + it('load LocalDatastoreController', () => { + const LocalDatastoreController = require('../LocalDatastoreController.react-native'); + require('../LocalDatastore'); + const LDC = CoreManager.getLocalDatastoreController(); + expect(LocalDatastoreController).toEqual(LDC); + }); + + it('load StorageController', () => { + const StorageController = require('../StorageController.react-native'); + jest.spyOn(StorageController, 'setItemAsync'); + const storage = require('../Storage'); + storage.setItemAsync('key', 'value'); + expect(StorageController.setItemAsync).toHaveBeenCalledTimes(1); + }); + + it('load WebSocketController', () => { + jest.mock('../EventEmitter', () => { + return require('events').EventEmitter; + }); + const socket = WebSocket; + require('../LiveQueryClient'); + const websocket = CoreManager.getWebSocketController(); + expect(websocket).toEqual(socket); + }); +}); diff --git a/src/__tests__/test_helpers/mockWeChat.js b/src/__tests__/test_helpers/mockWeChat.js new file mode 100644 index 000000000..d2e03b5d4 --- /dev/null +++ b/src/__tests__/test_helpers/mockWeChat.js @@ -0,0 +1,79 @@ +let mockStorage = {}; +let progressCallback = () => {}; +let socketOpenCallback = () => {}; +let socketMessageCallback = () => {}; +let socketCloseCallback = () => {}; +let SocketErrorCallback = () => {}; + +const mockWeChat = { + getStorageSync(path) { + return mockStorage[path]; + }, + + setStorageSync(path, value) { + mockStorage[path] = value; + }, + + removeStorageSync(path) { + delete mockStorage[path]; + }, + + getStorageInfoSync() { + return { + keys: Object.keys(mockStorage), + }; + }, + + clearStorageSync() { + mockStorage = {}; + }, + + request(options) { + return { + onProgressUpdate: (cb) => { + progressCallback = cb; + }, + abort: () => { + progressCallback({ + totalBytesExpectedToWrite: 0, + totalBytesWritten: 0, + }); + options.success({ + statusCode: 0, + data: {}, + }); + options.fail(); + } + } + }, + + connectSocket() {}, + + onSocketOpen(cb) { + socketOpenCallback = cb; + }, + + onSocketMessage(cb) { + socketMessageCallback = cb; + }, + + onSocketClose(cb) { + socketCloseCallback = cb; + }, + + onSocketError(cb) { + SocketErrorCallback = cb; + }, + + sendSocketMessage(data) { + socketOpenCallback(); + socketMessageCallback(data); + }, + + closeSocket() { + socketCloseCallback(); + SocketErrorCallback(); + }, +}; + +module.exports = mockWeChat; diff --git a/src/__tests__/unsavedChildren-test.js b/src/__tests__/unsavedChildren-test.js index 31084452e..c1f13ab10 100644 --- a/src/__tests__/unsavedChildren-test.js +++ b/src/__tests__/unsavedChildren-test.js @@ -30,6 +30,7 @@ jest.setMock('../ParseObject', mockObject); const ParseFile = require('../ParseFile').default; const ParseObject = require('../ParseObject'); +const ParseRelation = require('../ParseRelation').default; const unsavedChildren = require('../unsavedChildren').default; describe('unsavedChildren', () => { @@ -198,4 +199,16 @@ describe('unsavedChildren', () => { expect(unsavedChildren(a)).toEqual([ a.attributes.b ]); }); + + it('skips Relation', () => { + const relation = new ParseRelation(null, null); + const f = new ParseObject({ + className: 'Folder', + id: '121', + attributes: { + r: relation, + }, + }); + expect(unsavedChildren(f)).toEqual([]); + }); }); diff --git a/src/__tests__/weapp-test.js b/src/__tests__/weapp-test.js new file mode 100644 index 000000000..01293e08b --- /dev/null +++ b/src/__tests__/weapp-test.js @@ -0,0 +1,83 @@ +jest.dontMock('../CoreManager'); +jest.dontMock('../CryptoController'); +jest.dontMock('../decode'); +jest.dontMock('../encode'); +jest.dontMock('../EventEmitter'); +jest.dontMock('../LiveQueryClient'); +jest.dontMock('../Parse'); +jest.dontMock('../ParseFile'); +jest.dontMock('../ParseObject'); +jest.dontMock('../RESTController'); +jest.dontMock('../Socket.weapp'); +jest.dontMock('../Storage'); +jest.dontMock('crypto-js/aes'); +jest.dontMock('./test_helpers/mockWeChat'); + +const CoreManager = require('../CoreManager'); +const mockWeChat = require('./test_helpers/mockWeChat'); + +global.wx = mockWeChat; + +describe('WeChat', () => { + beforeEach(() => { + process.env.PARSE_BUILD = 'weapp'; + }); + + afterEach(() => { + process.env.PARSE_BUILD = 'node'; + }); + + it('load StorageController', () => { + const StorageController = require('../StorageController.weapp'); + jest.spyOn(StorageController, 'setItem'); + const storage = require('../Storage'); + storage.setItem('key', 'value'); + expect(StorageController.setItem).toHaveBeenCalledTimes(1); + }); + + it('load RESTController', () => { + const XHR = require('../Xhr.weapp'); + const RESTController = require('../RESTController'); + expect(RESTController._getXHR()).toEqual(XHR); + }); + + it('load ParseFile', () => { + const XHR = require('../Xhr.weapp'); + require('../ParseFile'); + const fileController = CoreManager.getFileController(); + expect(fileController._getXHR()).toEqual(XHR); + }); + + it('load WebSocketController', () => { + const socket = require('../Socket.weapp'); + require('../LiveQueryClient'); + const websocket = CoreManager.getWebSocketController(); + expect(websocket).toEqual(socket); + }); + + describe('Socket', () => { + it('send', () => { + const Websocket = require('../Socket.weapp'); + jest.spyOn(mockWeChat, 'connectSocket'); + const socket = new Websocket('wss://examples.com'); + socket.onopen(); + socket.onmessage(); + socket.onclose(); + socket.onerror(); + socket.onopen = jest.fn(); + socket.onmessage = jest.fn(); + socket.onclose = jest.fn(); + socket.onerror = jest.fn(); + + expect(mockWeChat.connectSocket).toHaveBeenCalled(); + + socket.send('{}'); + expect(socket.onopen).toHaveBeenCalled(); + expect(socket.onmessage).toHaveBeenCalled(); + + socket.close(); + expect(socket.onclose).toHaveBeenCalled(); + expect(socket.onerror).toHaveBeenCalled(); + }); + }); +});