const Parse = require('parse/node'); const ParseLiveQueryServer = require('../lib/LiveQuery/ParseLiveQueryServer').ParseLiveQueryServer; const ParseServer = require('../lib/ParseServer').default; const LiveQueryController = require('../lib/Controllers/LiveQueryController').LiveQueryController; const auth = require('../lib/Auth'); // Global mock info const queryHashValue = 'hash'; const testUserId = 'userId'; const testClassName = 'TestObject'; const timeout = () => jasmine.timeout(100); describe('ParseLiveQueryServer', function () { beforeEach(function (done) { // Mock ParseWebSocketServer const mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer'); jasmine.mockLibrary( '../lib/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer', mockParseWebSocketServer ); // Mock Client const mockClient = function (id, socket, hasMasterKey) { this.pushConnect = jasmine.createSpy('pushConnect'); this.pushSubscribe = jasmine.createSpy('pushSubscribe'); this.pushUnsubscribe = jasmine.createSpy('pushUnsubscribe'); this.pushDelete = jasmine.createSpy('pushDelete'); this.pushCreate = jasmine.createSpy('pushCreate'); this.pushEnter = jasmine.createSpy('pushEnter'); this.pushUpdate = jasmine.createSpy('pushUpdate'); this.pushLeave = jasmine.createSpy('pushLeave'); this.addSubscriptionInfo = jasmine.createSpy('addSubscriptionInfo'); this.getSubscriptionInfo = jasmine.createSpy('getSubscriptionInfo'); this.deleteSubscriptionInfo = jasmine.createSpy('deleteSubscriptionInfo'); this.hasMasterKey = hasMasterKey; }; mockClient.pushError = jasmine.createSpy('pushError'); jasmine.mockLibrary('../lib/LiveQuery/Client', 'Client', mockClient); // Mock Subscription const mockSubscriotion = function () { this.addClientSubscription = jasmine.createSpy('addClientSubscription'); this.deleteClientSubscription = jasmine.createSpy('deleteClientSubscription'); }; jasmine.mockLibrary('../lib/LiveQuery/Subscription', 'Subscription', mockSubscriotion); // Mock queryHash const mockQueryHash = jasmine.createSpy('matchesQuery').and.returnValue(queryHashValue); jasmine.mockLibrary('../lib/LiveQuery/QueryTools', 'queryHash', mockQueryHash); // Mock matchesQuery const mockMatchesQuery = jasmine.createSpy('matchesQuery').and.returnValue(true); jasmine.mockLibrary('../lib/LiveQuery/QueryTools', 'matchesQuery', mockMatchesQuery); // Mock ParsePubSub const mockParsePubSub = { createPublisher: function () { return { publish: jasmine.createSpy('publish'), on: jasmine.createSpy('on'), }; }, createSubscriber: function () { return { subscribe: jasmine.createSpy('subscribe'), on: jasmine.createSpy('on'), }; }, }; jasmine.mockLibrary('../lib/LiveQuery/ParsePubSub', 'ParsePubSub', mockParsePubSub); spyOn(auth, 'getAuthForSessionToken').and.callFake(({ sessionToken, cacheController }) => { if (typeof sessionToken === 'undefined') { return Promise.reject(); } if (sessionToken === null) { return Promise.reject(); } if (sessionToken === 'pleaseThrow') { return Promise.reject(); } if (sessionToken === 'invalid') { return Promise.reject( new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'invalid session token') ); } return Promise.resolve(new auth.Auth({ cacheController, user: { id: testUserId } })); }); done(); }); it('can be initialized', function () { const httpServer = {}; const parseLiveQueryServer = new ParseLiveQueryServer(httpServer); expect(parseLiveQueryServer.clientId).toBeUndefined(); expect(parseLiveQueryServer.clients.size).toBe(0); expect(parseLiveQueryServer.subscriptions.size).toBe(0); }); it('can be initialized from ParseServer', async () => { const httpServer = {}; const parseLiveQueryServer = await ParseServer.createLiveQueryServer(httpServer, {}); expect(parseLiveQueryServer.clientId).toBeUndefined(); expect(parseLiveQueryServer.clients.size).toBe(0); expect(parseLiveQueryServer.subscriptions.size).toBe(0); }); it('can be initialized from ParseServer without httpServer', async () => { const parseLiveQueryServer = await ParseServer.createLiveQueryServer(undefined, { port: 22345, }); expect(parseLiveQueryServer.clientId).toBeUndefined(); expect(parseLiveQueryServer.clients.size).toBe(0); expect(parseLiveQueryServer.subscriptions.size).toBe(0); await new Promise(resolve => parseLiveQueryServer.server.close(resolve)); }); describe_only_db('mongo')('initialization', () => { it('can be initialized through ParseServer without liveQueryServerOptions', async function (done) { const parseServer = await ParseServer.start({ appId: 'hello', masterKey: 'world', port: 22345, mountPath: '/1', serverURL: 'http://localhost:12345/1', liveQuery: { classNames: ['Yolo'], }, startLiveQueryServer: true, serverStartComplete: () => { expect(parseServer.liveQueryServer).not.toBeUndefined(); expect(parseServer.liveQueryServer.server).toBe(parseServer.server); parseServer.server.close(async () => { await reconfigureServer(); done(); }); }, }); }); it('can be initialized through ParseServer with liveQueryServerOptions', async function (done) { const parseServer = await ParseServer.start({ appId: 'hello', masterKey: 'world', port: 22346, mountPath: '/1', serverURL: 'http://localhost:12345/1', liveQuery: { classNames: ['Yolo'], }, liveQueryServerOptions: { port: 22347, }, serverStartComplete: () => { expect(parseServer.liveQueryServer).not.toBeUndefined(); expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server); parseServer.liveQueryServer.server.close( parseServer.server.close.bind(parseServer.server, async () => { await reconfigureServer(); done(); }) ); }, }); }); }); it('properly passes the CLP to afterSave/afterDelete hook', function (done) { function setPermissionsOnClass(className, permissions, doPut) { const request = require('request'); let op = request.post; if (doPut) { op = request.put; } return new Promise((resolve, reject) => { op( { url: Parse.serverURL + '/schemas/' + className, headers: { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': Parse.masterKey, }, json: true, body: { classLevelPermissions: permissions, }, }, (error, response, body) => { if (error) { return reject(error); } if (body.error) { return reject(body); } return resolve(body); } ); }); } let saveSpy; let deleteSpy; reconfigureServer({ liveQuery: { classNames: ['Yolo'], }, }) .then(parseServer => { saveSpy = spyOn(parseServer.config.liveQueryController, 'onAfterSave'); deleteSpy = spyOn(parseServer.config.liveQueryController, 'onAfterDelete'); return setPermissionsOnClass('Yolo', { create: { '*': true }, delete: { '*': true }, }); }) .then(() => { const obj = new Parse.Object('Yolo'); return obj.save(); }) .then(obj => { return obj.destroy(); }) .then(() => { expect(saveSpy).toHaveBeenCalled(); const saveArgs = saveSpy.calls.mostRecent().args; expect(saveArgs.length).toBe(4); expect(saveArgs[0]).toBe('Yolo'); expect(saveArgs[3]).toEqual({ get: {}, count: {}, addField: {}, create: { '*': true }, find: {}, update: {}, delete: { '*': true }, protectedFields: {}, }); expect(deleteSpy).toHaveBeenCalled(); const deleteArgs = deleteSpy.calls.mostRecent().args; expect(deleteArgs.length).toBe(4); expect(deleteArgs[0]).toBe('Yolo'); expect(deleteArgs[3]).toEqual({ get: {}, count: {}, addField: {}, create: { '*': true }, find: {}, update: {}, delete: { '*': true }, protectedFields: {}, }); done(); }) .catch(done.fail); }); it('can handle connect command', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); const parseWebSocket = { clientId: -1, }; parseLiveQueryServer._validateKeys = jasmine.createSpy('validateKeys').and.returnValue(true); await parseLiveQueryServer._handleConnect(parseWebSocket, { sessionToken: 'token', }); const clientKeys = parseLiveQueryServer.clients.keys(); expect(parseLiveQueryServer.clients.size).toBe(1); const firstKey = clientKeys.next().value; expect(parseWebSocket.clientId).toBe(firstKey); const client = parseLiveQueryServer.clients.get(firstKey); expect(client).not.toBeNull(); // Make sure we send connect response to the client expect(client.pushConnect).toHaveBeenCalled(); }); it('basic beforeConnect rejection', async () => { Parse.Cloud.beforeConnect(function () { throw new Error('You shall not pass!'); }); const parseLiveQueryServer = new ParseLiveQueryServer({}); const parseWebSocket = { clientId: -1, }; await parseLiveQueryServer._handleConnect(parseWebSocket, { sessionToken: 'token', }); expect(parseLiveQueryServer.clients.size).toBe(0); const Client = require('../lib/LiveQuery/Client').Client; expect(Client.pushError).toHaveBeenCalled(); }); it('basic beforeSubscribe rejection', async () => { Parse.Cloud.beforeSubscribe('test', function () { throw new Error('You shall not pass!'); }); const parseLiveQueryServer = new ParseLiveQueryServer({}); const parseWebSocket = { clientId: -1, }; await parseLiveQueryServer._handleConnect(parseWebSocket, { sessionToken: 'token', }); const query = { className: 'test', where: { key: 'value', }, fields: ['test'], }; const requestId = 2; const request = { query: query, requestId: requestId, sessionToken: 'sessionToken', }; await parseLiveQueryServer._handleSubscribe(parseWebSocket, request); expect(parseLiveQueryServer.clients.size).toBe(1); const Client = require('../lib/LiveQuery/Client').Client; expect(Client.pushError).toHaveBeenCalled(); }); it('can handle subscribe command without clientId', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); const incompleteParseConn = {}; await parseLiveQueryServer._handleSubscribe(incompleteParseConn, {}); const Client = require('../lib/LiveQuery/Client').Client; expect(Client.pushError).toHaveBeenCalled(); }); it('can handle subscribe command with new query', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; const client = addMockClient(parseLiveQueryServer, clientId); // Handle mock subscription const parseWebSocket = { clientId: clientId, }; const query = { className: 'test', where: { key: 'value', }, fields: ['test'], }; const requestId = 2; const request = { query: query, requestId: requestId, sessionToken: 'sessionToken', }; await parseLiveQueryServer._handleSubscribe(parseWebSocket, request); // Make sure we add the subscription to the server const subscriptions = parseLiveQueryServer.subscriptions; expect(subscriptions.size).toBe(1); expect(subscriptions.get(query.className)).not.toBeNull(); const classSubscriptions = subscriptions.get(query.className); expect(classSubscriptions.size).toBe(1); expect(classSubscriptions.get('hash')).not.toBeNull(); // TODO(check subscription constructor to verify we pass the right argument) // Make sure we add clientInfo to the subscription const subscription = classSubscriptions.get('hash'); expect(subscription.addClientSubscription).toHaveBeenCalledWith(clientId, requestId); // Make sure we add subscriptionInfo to the client const args = client.addSubscriptionInfo.calls.first().args; expect(args[0]).toBe(requestId); expect(args[1].fields).toBe(query.fields); expect(args[1].sessionToken).toBe(request.sessionToken); // Make sure we send subscribe response to the client expect(client.pushSubscribe).toHaveBeenCalledWith(requestId); }); it('can handle subscribe command with existing query', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add two mock clients const clientId = 1; addMockClient(parseLiveQueryServer, clientId); const clientIdAgain = 2; const clientAgain = addMockClient(parseLiveQueryServer, clientIdAgain); // Add subscription for mock client 1 const parseWebSocket = { clientId: clientId, }; const requestId = 2; const query = { className: 'test', where: { key: 'value', }, fields: ['test'], }; await addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query); // Add subscription for mock client 2 const parseWebSocketAgain = { clientId: clientIdAgain, }; const queryAgain = { className: 'test', where: { key: 'value', }, fields: ['testAgain'], }; const requestIdAgain = 1; await addMockSubscription( parseLiveQueryServer, clientIdAgain, requestIdAgain, parseWebSocketAgain, queryAgain ); // Make sure we only have one subscription const subscriptions = parseLiveQueryServer.subscriptions; expect(subscriptions.size).toBe(1); expect(subscriptions.get(query.className)).not.toBeNull(); const classSubscriptions = subscriptions.get(query.className); expect(classSubscriptions.size).toBe(1); expect(classSubscriptions.get('hash')).not.toBeNull(); // Make sure we add clientInfo to the subscription const subscription = classSubscriptions.get('hash'); // Make sure client 2 info has been added let args = subscription.addClientSubscription.calls.mostRecent().args; expect(args).toEqual([clientIdAgain, requestIdAgain]); // Make sure we add subscriptionInfo to the client 2 args = clientAgain.addSubscriptionInfo.calls.mostRecent().args; expect(args[0]).toBe(requestIdAgain); expect(args[1].fields).toBe(queryAgain.fields); }); it('can handle unsubscribe command without clientId', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); const incompleteParseConn = {}; parseLiveQueryServer._handleUnsubscribe(incompleteParseConn, {}); const Client = require('../lib/LiveQuery/Client').Client; expect(Client.pushError).toHaveBeenCalled(); }); it('can handle unsubscribe command without not existed client', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); const parseWebSocket = { clientId: 1, }; parseLiveQueryServer._handleUnsubscribe(parseWebSocket, {}); const Client = require('../lib/LiveQuery/Client').Client; expect(Client.pushError).toHaveBeenCalled(); }); it('can handle unsubscribe command without not existed query', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; addMockClient(parseLiveQueryServer, clientId); // Handle unsubscribe command const parseWebSocket = { clientId: 1, }; parseLiveQueryServer._handleUnsubscribe(parseWebSocket, {}); const Client = require('../lib/LiveQuery/Client').Client; expect(Client.pushError).toHaveBeenCalled(); }); it('can handle unsubscribe command', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Add mock client const clientId = 1; const client = addMockClient(parseLiveQueryServer, clientId); // Add subscription for mock client const parseWebSocket = { clientId: 1, }; const requestId = 2; const subscription = await addMockSubscription( parseLiveQueryServer, clientId, requestId, parseWebSocket ); // Mock client.getSubscriptionInfo const subscriptionInfo = client.addSubscriptionInfo.calls.mostRecent().args[1]; client.getSubscriptionInfo = function () { return subscriptionInfo; }; // Handle unsubscribe command const requestAgain = { requestId: requestId, }; parseLiveQueryServer._handleUnsubscribe(parseWebSocket, requestAgain); // Make sure we delete subscription from client expect(client.deleteSubscriptionInfo).toHaveBeenCalledWith(requestId); // Make sure we delete client from subscription expect(subscription.deleteClientSubscription).toHaveBeenCalledWith(clientId, requestId); // Make sure we clear subscription in the server const subscriptions = parseLiveQueryServer.subscriptions; expect(subscriptions.size).toBe(0); }); it('can set connect command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleConnect = jasmine.createSpy('_handleSubscribe'); // Make mock parseWebsocket const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); // Register message handlers for the parseWebSocket parseLiveQueryServer._onConnect(parseWebSocket); // Check connect request const connectRequest = { op: 'connect', applicationId: '1', installationId: '1234', }; // Trigger message event parseWebSocket.emit('message', connectRequest); // Make sure _handleConnect is called const args = parseLiveQueryServer._handleConnect.calls.mostRecent().args; expect(args[0]).toBe(parseWebSocket); }); it('can set subscribe command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleSubscribe = jasmine.createSpy('_handleSubscribe'); // Make mock parseWebsocket const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); // Register message handlers for the parseWebSocket parseLiveQueryServer._onConnect(parseWebSocket); // Check subscribe request const subscribeRequest = JSON.stringify({ op: 'subscribe', requestId: 1, query: { className: 'Test', where: {} }, }); // Trigger message event parseWebSocket.emit('message', subscribeRequest); // Make sure _handleSubscribe is called const args = parseLiveQueryServer._handleSubscribe.calls.mostRecent().args; expect(args[0]).toBe(parseWebSocket); expect(JSON.stringify(args[1])).toBe(subscribeRequest); }); it('can set unsubscribe command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server parseLiveQueryServer._handleUnsubscribe = jasmine.createSpy('_handleSubscribe'); // Make mock parseWebsocket const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); // Register message handlers for the parseWebSocket parseLiveQueryServer._onConnect(parseWebSocket); // Check unsubscribe request const unsubscribeRequest = JSON.stringify({ op: 'unsubscribe', requestId: 1, }); // Trigger message event parseWebSocket.emit('message', unsubscribeRequest); // Make sure _handleUnsubscribe is called const args = parseLiveQueryServer._handleUnsubscribe.calls.mostRecent().args; expect(args[0]).toBe(parseWebSocket); expect(JSON.stringify(args[1])).toBe(unsubscribeRequest); }); it('can set update command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Register mock connect/subscribe/unsubscribe handler for the server spyOn(parseLiveQueryServer, '_handleUpdateSubscription').and.callThrough(); spyOn(parseLiveQueryServer, '_handleUnsubscribe').and.callThrough(); spyOn(parseLiveQueryServer, '_handleSubscribe').and.callThrough(); // Make mock parseWebsocket const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); // Register message handlers for the parseWebSocket parseLiveQueryServer._onConnect(parseWebSocket); // Check updateRequest request const updateRequest = JSON.stringify({ op: 'update', requestId: 1, query: { className: 'Test', where: {} }, }); // Trigger message event parseWebSocket.emit('message', updateRequest); // Make sure _handleUnsubscribe is called const args = parseLiveQueryServer._handleUpdateSubscription.calls.mostRecent().args; expect(args[0]).toBe(parseWebSocket); expect(JSON.stringify(args[1])).toBe(updateRequest); expect(parseLiveQueryServer._handleUnsubscribe).toHaveBeenCalled(); const unsubArgs = parseLiveQueryServer._handleUnsubscribe.calls.mostRecent().args; expect(unsubArgs.length).toBe(3); expect(unsubArgs[2]).toBe(false); expect(parseLiveQueryServer._handleSubscribe).toHaveBeenCalled(); }); it('can set missing command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock parseWebsocket const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); // Register message handlers for the parseWebSocket parseLiveQueryServer._onConnect(parseWebSocket); // Check invalid request const invalidRequest = '{}'; // Trigger message event parseWebSocket.emit('message', invalidRequest); const Client = require('../lib/LiveQuery/Client').Client; expect(Client.pushError).toHaveBeenCalled(); }); it('can set unknown command message handler for a parseWebSocket', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock parseWebsocket const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); // Register message handlers for the parseWebSocket parseLiveQueryServer._onConnect(parseWebSocket); // Check unknown request const unknownRequest = '{"op":"unknown"}'; // Trigger message event parseWebSocket.emit('message', unknownRequest); const Client = require('../lib/LiveQuery/Client').Client; expect(Client.pushError).toHaveBeenCalled(); }); it('can set disconnect command message handler for a parseWebSocket which has not registered to the server', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); parseWebSocket.clientId = 1; // Register message handlers for the parseWebSocket parseLiveQueryServer._onConnect(parseWebSocket); // Make sure we do not crash // Trigger disconnect event parseWebSocket.emit('disconnect'); }); it('can forward event to cloud code', function () { const cloudCodeHandler = { handler: () => {}, }; const spy = spyOn(cloudCodeHandler, 'handler').and.callThrough(); Parse.Cloud.onLiveQueryEvent(cloudCodeHandler.handler); const parseLiveQueryServer = new ParseLiveQueryServer({}); const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); parseWebSocket.clientId = 1; // Register message handlers for the parseWebSocket parseLiveQueryServer._onConnect(parseWebSocket); // Make sure we do not crash // Trigger disconnect event parseWebSocket.emit('disconnect'); expect(spy).toHaveBeenCalled(); // call for ws_connect, another for ws_disconnect expect(spy.calls.count()).toBe(2); }); // TODO: Test server can set disconnect command message handler for a parseWebSocket it('has no subscription and can handle object delete command', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); parseObject._finishFetch({ key: 'value', className: testClassName, }); // Make mock message const message = { currentParseObject: parseObject, }; // Make sure we do not crash in this case parseLiveQueryServer._onAfterDelete(message, {}); }); it('can handle object delete command which does not match any subscription', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); parseObject._finishFetch({ key: 'value', className: testClassName, }); // Make mock message const message = { currentParseObject: parseObject, }; // Add mock client const clientId = 1; addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; await addMockSubscription(parseLiveQueryServer, clientId, requestId); const client = parseLiveQueryServer.clients.get(clientId); // Mock _matchesSubscription to return not matching parseLiveQueryServer._matchesSubscription = function () { return false; }; parseLiveQueryServer._matchesACL = function () { return true; }; parseLiveQueryServer._onAfterDelete(message); // Make sure we do not send command to client expect(client.pushDelete).not.toHaveBeenCalled(); }); it('can handle object delete command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make deletedParseObject const parseObject = new Parse.Object(testClassName); parseObject._finishFetch({ key: 'value', className: testClassName, }); // Make mock message const message = { currentParseObject: parseObject, }; // Add mock client const clientId = 1; addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; await addMockSubscription(parseLiveQueryServer, clientId, requestId); const client = parseLiveQueryServer.clients.get(clientId); // Mock _matchesSubscription to return matching parseLiveQueryServer._matchesSubscription = function () { return true; }; parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterDelete(message); // Make sure we send command to client, since _matchesACL is async, we have to // wait and check await timeout(); expect(client.pushDelete).toHaveBeenCalled(); done(); }); it('has no subscription and can handle object save command', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); // Make sure we do not crash in this case parseLiveQueryServer._onAfterSave(message); }); it('can handle object save command which does not match any subscription', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); // Add mock client const clientId = 1; const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return not matching parseLiveQueryServer._matchesSubscription = function () { return false; }; parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; // Trigger onAfterSave parseLiveQueryServer._onAfterSave(message); // Make sure we do not send command to client await timeout(); expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); expect(client.pushDelete).not.toHaveBeenCalled(); expect(client.pushLeave).not.toHaveBeenCalled(); done(); }); it('can handle object enter command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); // Add mock client const clientId = 1; const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return matching // In order to mimic a enter, we need original match return false // and the current match return true let counter = 0; parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } counter += 1; return counter % 2 === 0; }; parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send enter command to client await timeout(); expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); expect(client.pushDelete).not.toHaveBeenCalled(); expect(client.pushLeave).not.toHaveBeenCalled(); done(); }); it('can handle object update command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); // Add mock client const clientId = 1; const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return matching parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } return true; }; parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send update command to client await timeout(); expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).toHaveBeenCalled(); expect(client.pushDelete).not.toHaveBeenCalled(); expect(client.pushLeave).not.toHaveBeenCalled(); done(); }); it('can handle object leave command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); // Add mock client const clientId = 1; const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return matching // In order to mimic a leave, we need original match return true // and the current match return false let counter = 0; parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } counter += 1; return counter % 2 !== 0; }; parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send leave command to client await timeout(); expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); expect(client.pushDelete).not.toHaveBeenCalled(); expect(client.pushLeave).toHaveBeenCalled(); done(); }); it('sends correct events for object with multiple subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); Parse.Cloud.afterLiveQueryEvent('TestObject', () => { // Simulate delay due to trigger, auth, etc. return jasmine.timeout(10); }); // Make mock request message const message = generateMockMessage(true); // Add mock client const clientId = 1; const client = addMockClient(parseLiveQueryServer, clientId); client.sessionToken = 'sessionToken'; // Mock queryHash for this special test const mockQueryHash = jasmine.createSpy('matchesQuery').and.returnValue('hash1'); jasmine.mockLibrary('../lib/LiveQuery/QueryTools', 'queryHash', mockQueryHash); // Add mock subscription 1 const requestId2 = 2; await addMockSubscription(parseLiveQueryServer, clientId, requestId2, null, null, 'hash1'); // Mock queryHash for this special test const mockQueryHash2 = jasmine.createSpy('matchesQuery').and.returnValue('hash2'); jasmine.mockLibrary('../lib/LiveQuery/QueryTools', 'queryHash', mockQueryHash2); // Add mock subscription 2 const requestId3 = 3; await addMockSubscription(parseLiveQueryServer, clientId, requestId3, null, null, 'hash2'); // Mock _matchesSubscription to return matching // In order to mimic a leave, then enter, we need original match return true // and the current match return false, then the other way around let counter = 0; parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } counter += 1; // true, false, false, true return counter < 2 || counter > 3; }; parseLiveQueryServer._matchesACL = function () { // Simulate call return jasmine.timeout(10).then(() => true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send leave and enter command to client await timeout(); expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).toHaveBeenCalledTimes(1); expect(client.pushEnter).toHaveBeenCalledWith( requestId3, { key: 'value', className: 'TestObject' }, { key: 'originalValue', className: 'TestObject' } ); expect(client.pushUpdate).not.toHaveBeenCalled(); expect(client.pushDelete).not.toHaveBeenCalled(); expect(client.pushLeave).toHaveBeenCalledTimes(1); expect(client.pushLeave).toHaveBeenCalledWith( requestId2, { key: 'value', className: 'TestObject' }, { key: 'originalValue', className: 'TestObject' } ); done(); }); it('can handle update command with original object', async done => { jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client'); const Client = require('../lib/LiveQuery/Client').Client; const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(true); const clientId = 1; const parseWebSocket = { clientId, send: jasmine.createSpy('send'), }; const client = new Client(clientId, parseWebSocket); spyOn(client, 'pushUpdate').and.callThrough(); parseLiveQueryServer.clients.set(clientId, client); // Add mock subscription const requestId = 2; await addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket); // Mock _matchesSubscription to return matching parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } return true; }; parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send update command to client await timeout(); expect(client.pushUpdate).toHaveBeenCalled(); const args = parseWebSocket.send.calls.mostRecent().args; const toSend = JSON.parse(args[0]); expect(toSend.object).toBeDefined(); expect(toSend.original).toBeDefined(); done(); }); it('can handle object create command which matches some subscriptions', async done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); // Add mock client const clientId = 1; const client = addMockClient(parseLiveQueryServer, clientId); // Add mock subscription const requestId = 2; await addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return matching parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } return true; }; parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send create command to client await timeout(); expect(client.pushCreate).toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); expect(client.pushDelete).not.toHaveBeenCalled(); expect(client.pushLeave).not.toHaveBeenCalled(); done(); }); it('can handle create command with fields', async done => { jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client'); const Client = require('../lib/LiveQuery/Client').Client; const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request message const message = generateMockMessage(); const clientId = 1; const parseWebSocket = { clientId, send: jasmine.createSpy('send'), }; const client = new Client(clientId, parseWebSocket); spyOn(client, 'pushCreate').and.callThrough(); parseLiveQueryServer.clients.set(clientId, client); // Add mock subscription const requestId = 2; const query = { className: testClassName, where: { key: 'value', }, fields: ['test'], }; await addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query); // Mock _matchesSubscription to return matching parseLiveQueryServer._matchesSubscription = function (parseObject) { if (!parseObject) { return false; } return true; }; parseLiveQueryServer._matchesACL = function () { return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send create command to client await timeout(); expect(client.pushCreate).toHaveBeenCalled(); const args = parseWebSocket.send.calls.mostRecent().args; const toSend = JSON.parse(args[0]); expect(toSend.object).toBeDefined(); expect(toSend.original).toBeUndefined(); done(); }); it('can match subscription for null or undefined parse object', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock subscription const subscription = { match: jasmine.createSpy('match'), }; expect(parseLiveQueryServer._matchesSubscription(null, subscription)).toBe(false); expect(parseLiveQueryServer._matchesSubscription(undefined, subscription)).toBe(false); // Make sure subscription.match is not called expect(subscription.match).not.toHaveBeenCalled(); }); it('can match subscription', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock subscription const subscription = { query: {}, }; const parseObject = {}; expect(parseLiveQueryServer._matchesSubscription(parseObject, subscription)).toBe(true); // Make sure matchesQuery is called const matchesQuery = require('../lib/LiveQuery/QueryTools').matchesQuery; expect(matchesQuery).toHaveBeenCalledWith(parseObject, subscription.query); }); it('can inflate parse object', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}); // Make mock request const objectJSON = { className: 'testClassName', createdAt: '2015-12-22T01:51:12.955Z', key: 'value', objectId: 'BfwxBCz6yW', updatedAt: '2016-01-05T00:46:45.659Z', }; const originalObjectJSON = { className: 'testClassName', createdAt: '2015-12-22T01:51:12.955Z', key: 'originalValue', objectId: 'BfwxBCz6yW', updatedAt: '2016-01-05T00:46:45.659Z', }; const message = { currentParseObject: objectJSON, originalParseObject: originalObjectJSON, }; // Inflate the object parseLiveQueryServer._inflateParseObject(message); // Verify object const object = message.currentParseObject; expect(object instanceof Parse.Object).toBeTruthy(); expect(object.get('key')).toEqual('value'); expect(object.className).toEqual('testClassName'); expect(object.id).toBe('BfwxBCz6yW'); expect(object.createdAt).not.toBeUndefined(); expect(object.updatedAt).not.toBeUndefined(); // Verify original object const originalObject = message.originalParseObject; expect(originalObject instanceof Parse.Object).toBeTruthy(); expect(originalObject.get('key')).toEqual('originalValue'); expect(originalObject.className).toEqual('testClassName'); expect(originalObject.id).toBe('BfwxBCz6yW'); expect(originalObject.createdAt).not.toBeUndefined(); expect(originalObject.updatedAt).not.toBeUndefined(); }); it('can inflate user object', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); const userJSON = { username: 'test', ACL: {}, createdAt: '2018-12-21T23:09:51.784Z', sessionToken: 'r:1234', updatedAt: '2018-12-21T23:09:51.784Z', objectId: 'NhF2u9n72W', __type: 'Object', className: '_User', _hashed_password: '1234', _email_verify_token: '1234', }; const originalUserJSON = { username: 'test', ACL: {}, createdAt: '2018-12-21T23:09:51.784Z', sessionToken: 'r:1234', updatedAt: '2018-12-21T23:09:51.784Z', objectId: 'NhF2u9n72W', __type: 'Object', className: '_User', _hashed_password: '12345', _email_verify_token: '12345', }; const message = { currentParseObject: userJSON, originalParseObject: originalUserJSON, }; parseLiveQueryServer._inflateParseObject(message); const object = message.currentParseObject; expect(object instanceof Parse.Object).toBeTruthy(); expect(object.get('_hashed_password')).toBeUndefined(); expect(object.get('_email_verify_token')).toBeUndefined(); expect(object.className).toEqual('_User'); expect(object.id).toBe('NhF2u9n72W'); expect(object.createdAt).not.toBeUndefined(); expect(object.updatedAt).not.toBeUndefined(); const originalObject = message.originalParseObject; expect(originalObject instanceof Parse.Object).toBeTruthy(); expect(originalObject.get('_hashed_password')).toBeUndefined(); expect(originalObject.get('_email_verify_token')).toBeUndefined(); expect(originalObject.className).toEqual('_User'); expect(originalObject.id).toBe('NhF2u9n72W'); expect(originalObject.createdAt).not.toBeUndefined(); expect(originalObject.updatedAt).not.toBeUndefined(); }); it('can match undefined ACL', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const client = {}; const requestId = 0; parseLiveQueryServer._matchesACL(undefined, client, requestId).then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); it('can match ACL with none exist requestId', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); const client = { getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue(undefined), }; const requestId = 0; parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); it('can match ACL with public read access', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(true); const client = { getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: 'sessionToken', }), }; const requestId = 0; parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); it('can match ACL with valid subscription sessionToken', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); const client = { getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: 'sessionToken', }), }; const requestId = 0; parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); it('can match ACL with valid client sessionToken', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined const client = { sessionToken: 'sessionToken', getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: undefined, }), }; const requestId = 0; parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); it('can match ACL with invalid subscription and client sessionToken', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined const client = { sessionToken: undefined, getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: undefined, }), }; const requestId = 0; parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); it('can match ACL with subscription sessionToken checking error', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return error when sessionToken is null, this is just // the behaviour of our mock sessionTokenCache, not real sessionTokenCache const client = { getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: null, }), }; const requestId = 0; parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); it('can match ACL with client sessionToken checking error', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return error when sessionToken is null const client = { sessionToken: null, getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: null, }), }; const requestId = 0; parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); it("won't match ACL that doesn't have public read or any roles", function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); const client = { getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: 'sessionToken', }), }; const requestId = 0; parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); it("won't match non-public ACL with role when there is no user", function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); acl.setRoleReadAccess('livequery', true); const client = { getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({}), }; const requestId = 0; parseLiveQueryServer ._matchesACL(acl, client, requestId) .then(function (isMatched) { expect(isMatched).toBe(false); done(); }) .catch(done.fail); }); it("won't match ACL with role based read access set to false", function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); acl.setRoleReadAccess('otherLiveQueryRead', true); const client = { getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: 'sessionToken', }), }; const requestId = 0; spyOn(Parse, 'Query').and.callFake(function () { let shouldReturn = false; return { equalTo() { shouldReturn = true; // Nothing to do here return this; }, containedIn() { shouldReturn = false; return this; }, find() { if (!shouldReturn) { return Promise.resolve([]); } //Return a role with the name "liveQueryRead" as that is what was set on the ACL const liveQueryRole = new Parse.Role('liveQueryRead', new Parse.ACL()); liveQueryRole.id = 'abcdef1234'; return Promise.resolve([liveQueryRole]); }, }; }); parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(false); done(); }); parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); it('will match ACL with role based read access set to true', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); acl.setRoleReadAccess('liveQueryRead', true); const client = { getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: 'sessionToken', }), }; const requestId = 0; spyOn(Parse, 'Query').and.callFake(function () { let shouldReturn = false; return { equalTo() { shouldReturn = true; // Nothing to do here return this; }, containedIn() { shouldReturn = false; return this; }, find() { if (!shouldReturn) { return Promise.resolve([]); } //Return a role with the name "liveQueryRead" as that is what was set on the ACL const liveQueryRole = new Parse.Role('liveQueryRead', new Parse.ACL()); liveQueryRole.id = 'abcdef1234'; return Promise.resolve([liveQueryRole]); }, each(callback) { //Return a role with the name "liveQueryRead" as that is what was set on the ACL const liveQueryRole = new Parse.Role('liveQueryRead', new Parse.ACL()); liveQueryRole.id = 'abcdef1234'; callback(liveQueryRole); return Promise.resolve(); }, }; }); parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); describe('class level permissions', () => { it('matches CLP when find is closed', done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined const client = { sessionToken: 'sessionToken', getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: undefined, }), }; const requestId = 0; parseLiveQueryServer ._matchesCLP( { find: {}, }, { className: 'Yolo' }, client, requestId, 'find' ) .then(isMatched => { expect(isMatched).toBe(false); done(); }); }); it('matches CLP when find is open', done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined const client = { sessionToken: 'sessionToken', getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: undefined, }), }; const requestId = 0; parseLiveQueryServer ._matchesCLP( { find: { '*': true }, }, { className: 'Yolo' }, client, requestId, 'find' ) .then(isMatched => { expect(isMatched).toBe(true); done(); }); }); it('matches CLP when find is restricted to userIds', done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined const client = { sessionToken: 'sessionToken', getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: 'userId', }), }; const requestId = 0; parseLiveQueryServer ._matchesCLP( { find: { userId: true }, }, { className: 'Yolo' }, client, requestId, 'find' ) .then(isMatched => { expect(isMatched).toBe(true); done(); }); }); it('matches CLP when find is restricted to userIds', done => { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); // Mock sessionTokenCache will return false when sessionToken is undefined const client = { sessionToken: 'sessionToken', getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ sessionToken: undefined, }), }; const requestId = 0; parseLiveQueryServer ._matchesCLP( { find: { userId: true }, }, { className: 'Yolo' }, client, requestId, 'find' ) .then(isMatched => { expect(isMatched).toBe(false); done(); }); }); }); it('can validate key when valid key is provided', function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { keyPairs: { clientKey: 'test', }, } ); const request = { clientKey: 'test', }; expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).toBeTruthy(); }); it('can validate key when invalid key is provided', function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { keyPairs: { clientKey: 'test', }, } ); const request = { clientKey: 'error', }; expect( parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs) ).not.toBeTruthy(); }); it('can validate key when key is not provided', function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { keyPairs: { clientKey: 'test', }, } ); const request = {}; expect( parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs) ).not.toBeTruthy(); }); it('can validate key when validKerPairs is empty', function () { const parseLiveQueryServer = new ParseLiveQueryServer({}, {}); const request = {}; expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).toBeTruthy(); }); it('can validate client has master key when valid', function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { keyPairs: { masterKey: 'test', }, } ); const request = { masterKey: 'test', }; expect(parseLiveQueryServer._hasMasterKey(request, parseLiveQueryServer.keyPairs)).toBeTruthy(); }); it("can validate client doesn't have master key when invalid", function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { keyPairs: { masterKey: 'test', }, } ); const request = { masterKey: 'notValid', }; expect( parseLiveQueryServer._hasMasterKey(request, parseLiveQueryServer.keyPairs) ).not.toBeTruthy(); }); it("can validate client doesn't have master key when not provided", function () { const parseLiveQueryServer = new ParseLiveQueryServer( {}, { keyPairs: { masterKey: 'test', }, } ); expect(parseLiveQueryServer._hasMasterKey({}, parseLiveQueryServer.keyPairs)).not.toBeTruthy(); }); it("can validate client doesn't have master key when validKeyPairs is empty", function () { const parseLiveQueryServer = new ParseLiveQueryServer({}, {}); const request = { masterKey: 'test', }; expect( parseLiveQueryServer._hasMasterKey(request, parseLiveQueryServer.keyPairs) ).not.toBeTruthy(); }); it('will match non-public ACL when client has master key', function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); const client = { getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({}), hasMasterKey: true, }; const requestId = 0; parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(true); done(); }); }); it("won't match non-public ACL when client has no master key", function (done) { const parseLiveQueryServer = new ParseLiveQueryServer({}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); const client = { getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({}), hasMasterKey: false, }; const requestId = 0; parseLiveQueryServer._matchesACL(acl, client, requestId).then(function (isMatched) { expect(isMatched).toBe(false); done(); }); }); it('should properly pull auth from cache', () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); const promise = parseLiveQueryServer.getAuthForSessionToken('sessionToken'); const secondPromise = parseLiveQueryServer.getAuthForSessionToken('sessionToken'); // should be in the cache expect(parseLiveQueryServer.authCache.get('sessionToken')).toBe(promise); // should be the same promise returned expect(promise).toBe(secondPromise); // the auth should be called only once expect(auth.getAuthForSessionToken.calls.count()).toBe(1); }); it('should delete from cache throwing auth calls', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); const promise = parseLiveQueryServer.getAuthForSessionToken('pleaseThrow'); expect(parseLiveQueryServer.authCache.get('pleaseThrow')).toBe(promise); // after the promise finishes, it should have removed it from the cache expect(await promise).toEqual({}); expect(parseLiveQueryServer.authCache.get('pleaseThrow')).toBe(undefined); }); it('should keep a cache of invalid sessions', async () => { const parseLiveQueryServer = new ParseLiveQueryServer({}); const promise = parseLiveQueryServer.getAuthForSessionToken('invalid'); expect(parseLiveQueryServer.authCache.get('invalid')).toBe(promise); // after the promise finishes, it should have removed it from the cache await promise; const finalResult = await parseLiveQueryServer.authCache.get('invalid'); expect(finalResult.error).not.toBeUndefined(); expect(parseLiveQueryServer.authCache.get('invalid')).not.toBe(undefined); }); afterEach(function () { jasmine.restoreLibrary('../lib/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer'); jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client'); jasmine.restoreLibrary('../lib/LiveQuery/Subscription', 'Subscription'); jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'queryHash'); jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'matchesQuery'); jasmine.restoreLibrary('../lib/LiveQuery/ParsePubSub', 'ParsePubSub'); }); // Helper functions to add mock client and subscription to a liveQueryServer function addMockClient(parseLiveQueryServer, clientId) { const Client = require('../lib/LiveQuery/Client').Client; const client = new Client(clientId, {}); parseLiveQueryServer.clients.set(clientId, client); return client; } async function addMockSubscription( parseLiveQueryServer, clientId, requestId, parseWebSocket, query, customQueryHashValue ) { // If parseWebSocket is null, we use the default one if (!parseWebSocket) { const EventEmitter = require('events'); parseWebSocket = new EventEmitter(); } parseWebSocket.clientId = clientId; // If query is null, we use the default one if (!query) { query = { className: testClassName, where: { key: 'value', }, fields: ['test'], }; } const request = { query: query, requestId: requestId, sessionToken: 'sessionToken', }; await parseLiveQueryServer._handleSubscribe(parseWebSocket, request); // Make mock subscription const subscription = parseLiveQueryServer.subscriptions .get(query.className) .get(customQueryHashValue || queryHashValue); subscription.hasSubscribingClient = function () { return false; }; subscription.className = query.className; subscription.hash = customQueryHashValue || queryHashValue; if (subscription.clientRequestIds && subscription.clientRequestIds.has(clientId)) { subscription.clientRequestIds.get(clientId).push(requestId); } else { subscription.clientRequestIds = new Map([[clientId, [requestId]]]); } return subscription; } // Helper functiosn to generate request message function generateMockMessage(hasOriginalParseObject) { const parseObject = new Parse.Object(testClassName); parseObject._finishFetch({ key: 'value', className: testClassName, }); const message = { currentParseObject: parseObject, }; if (hasOriginalParseObject) { const originalParseObject = new Parse.Object(testClassName); originalParseObject._finishFetch({ key: 'originalValue', className: testClassName, }); message.originalParseObject = originalParseObject; } return message; } }); describe('LiveQueryController', () => { it('properly passes the CLP to afterSave/afterDelete hook', function (done) { function setPermissionsOnClass(className, permissions, doPut) { const request = require('request'); let op = request.post; if (doPut) { op = request.put; } return new Promise((resolve, reject) => { op( { url: Parse.serverURL + '/schemas/' + className, headers: { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': Parse.masterKey, }, json: true, body: { classLevelPermissions: permissions, }, }, (error, response, body) => { if (error) { return reject(error); } if (body.error) { return reject(body); } return resolve(body); } ); }); } let saveSpy; let deleteSpy; reconfigureServer({ liveQuery: { classNames: ['Yolo'], }, }) .then(parseServer => { saveSpy = spyOn(parseServer.config.liveQueryController, 'onAfterSave').and.callThrough(); deleteSpy = spyOn( parseServer.config.liveQueryController, 'onAfterDelete' ).and.callThrough(); return setPermissionsOnClass('Yolo', { create: { '*': true }, delete: { '*': true }, }); }) .then(() => { const obj = new Parse.Object('Yolo'); return obj.save(); }) .then(obj => { return obj.destroy(); }) .then(() => { expect(saveSpy).toHaveBeenCalled(); const saveArgs = saveSpy.calls.mostRecent().args; expect(saveArgs.length).toBe(4); expect(saveArgs[0]).toBe('Yolo'); expect(saveArgs[3]).toEqual({ get: {}, count: {}, addField: {}, create: { '*': true }, find: {}, update: {}, delete: { '*': true }, protectedFields: {}, }); expect(deleteSpy).toHaveBeenCalled(); const deleteArgs = deleteSpy.calls.mostRecent().args; expect(deleteArgs.length).toBe(4); expect(deleteArgs[0]).toBe('Yolo'); expect(deleteArgs[3]).toEqual({ get: {}, count: {}, addField: {}, create: { '*': true }, find: {}, update: {}, delete: { '*': true }, protectedFields: {}, }); done(); }) .catch(done.fail); }); it('should properly pack message request on afterSave', () => { const controller = new LiveQueryController({ classNames: ['Yolo'], }); const spy = spyOn(controller.liveQueryPublisher, 'onCloudCodeAfterSave'); controller.onAfterSave('Yolo', { o: 1 }, { o: 2 }, { yolo: true }); expect(spy).toHaveBeenCalled(); const args = spy.calls.mostRecent().args; expect(args.length).toBe(1); expect(args[0]).toEqual({ object: { o: 1 }, original: { o: 2 }, classLevelPermissions: { yolo: true }, }); }); it('should properly pack message request on afterDelete', () => { const controller = new LiveQueryController({ classNames: ['Yolo'], }); const spy = spyOn(controller.liveQueryPublisher, 'onCloudCodeAfterDelete'); controller.onAfterDelete('Yolo', { o: 1 }, { o: 2 }, { yolo: true }); expect(spy).toHaveBeenCalled(); const args = spy.calls.mostRecent().args; expect(args.length).toBe(1); expect(args[0]).toEqual({ object: { o: 1 }, original: { o: 2 }, classLevelPermissions: { yolo: true }, }); }); it('should properly pack message request', () => { const controller = new LiveQueryController({ classNames: ['Yolo'], }); expect(controller._makePublisherRequest({})).toEqual({ object: {}, original: undefined, }); }); });