diff --git a/integration/server.js b/integration/server.js index 436295af6..03efd23af 100644 --- a/integration/server.js +++ b/integration/server.js @@ -9,7 +9,11 @@ const api = new ParseServer({ appId: 'integration', masterKey: 'notsosecret', serverURL: 'http://localhost:1337/parse', // Don't forget to change to https if needed - cloud: `${__dirname}/cloud/main.js` + cloud: `${__dirname}/cloud/main.js`, + liveQuery: { + classNames: ['TestObject', 'DiffObject'], + }, + startLiveQueryServer: true, }); // Serve the Parse API on the /parse URL prefix diff --git a/integration/test/ParseLiveQueryTest.js b/integration/test/ParseLiveQueryTest.js new file mode 100644 index 000000000..535a73fe4 --- /dev/null +++ b/integration/test/ParseLiveQueryTest.js @@ -0,0 +1,148 @@ +'use strict'; + +const assert = require('assert'); +const clear = require('./clear'); +const Parse = require('../../node'); + +const TestObject = Parse.Object.extend('TestObject'); +const DiffObject = Parse.Object.extend('DiffObject'); + +describe('Parse LiveQuery', () => { + beforeEach((done) => { + Parse.initialize('integration'); + Parse.CoreManager.set('SERVER_URL', 'http://localhost:1337/parse'); + Parse.Storage._clear(); + clear().then(done).catch(done.fail); + }); + + it('can subscribe to query', async () => { + const object = new TestObject(); + await object.save(); + + const query = new Parse.Query(TestObject); + query.equalTo('objectId', object.id); + const subscription = await query.subscribe(); + + subscription.on('update', object => { + assert.equal(object.get('foo'), 'bar'); + }) + object.set({ foo: 'bar' }); + await object.save(); + }); + + it('can subscribe to multiple queries', async (done) => { + const objectA = new TestObject(); + const objectB = new TestObject(); + await Parse.Object.saveAll([objectA, objectB]); + + const queryA = new Parse.Query(TestObject); + const queryB = new Parse.Query(TestObject); + queryA.equalTo('objectId', objectA.id); + queryB.equalTo('objectId', objectB.id); + const subscriptionA = await queryA.subscribe(); + const subscriptionB = await queryB.subscribe(); + let count = 0; + subscriptionA.on('update', object => { + count++; + assert.equal(object.get('foo'), 'bar'); + }) + subscriptionB.on('update', object => { + count++; + assert.equal(object.get('foo'), 'baz'); + }) + await objectA.save({ foo: 'bar' }); + await objectB.save({ foo: 'baz' }); + + setTimeout(() => { + assert.equal(count, 2); + done(); + }, 100); + }); + + it('can subscribe to multiple queries different class', async (done) => { + const objectA = new TestObject(); + const objectB = new DiffObject(); + await Parse.Object.saveAll([objectA, objectB]); + + const queryA = new Parse.Query(TestObject); + const queryB = new Parse.Query(DiffObject); + queryA.equalTo('objectId', objectA.id); + queryB.equalTo('objectId', objectB.id); + const subscriptionA = await queryA.subscribe(); + const subscriptionB = await queryB.subscribe(); + let count = 0; + subscriptionA.on('update', object => { + count++; + assert.equal(object.get('foo'), 'bar'); + }) + subscriptionB.on('update', object => { + count++; + assert.equal(object.get('foo'), 'baz'); + }) + await objectA.save({ foo: 'bar' }); + await objectB.save({ foo: 'baz' }); + + setTimeout(() => { + expect(count).toBe(2); + done(); + }, 1000); + }); + + it('can unsubscribe to multiple queries different class', async (done) => { + const objectA = new TestObject(); + const objectB = new DiffObject(); + await Parse.Object.saveAll([objectA, objectB]); + + const queryA = new Parse.Query(TestObject); + const queryB = new Parse.Query(DiffObject); + queryA.equalTo('objectId', objectA.id); + queryB.equalTo('objectId', objectB.id); + const subscriptionA = await queryA.subscribe(); + const subscriptionB = await queryB.subscribe(); + let count = 0; + subscriptionA.on('update', () => { + count++; + }) + subscriptionB.on('update', object => { + count++; + assert.equal(object.get('foo'), 'baz'); + }) + subscriptionA.unsubscribe(); + await objectA.save({ foo: 'bar' }); + await objectB.save({ foo: 'baz' }); + + setTimeout(() => { + assert.equal(count, 1); + done(); + }, 1000); + }); + + it('can unsubscribe with await to multiple queries different class', async (done) => { + const objectA = new TestObject(); + const objectB = new DiffObject(); + await Parse.Object.saveAll([objectA, objectB]); + + const queryA = new Parse.Query(TestObject); + const queryB = new Parse.Query(DiffObject); + queryA.equalTo('objectId', objectA.id); + queryB.equalTo('objectId', objectB.id); + const subscriptionA = await queryA.subscribe(); + const subscriptionB = await queryB.subscribe(); + let count = 0; + subscriptionA.on('update', () => { + count++; + }) + subscriptionB.on('update', object => { + count++; + assert.equal(object.get('foo'), 'baz'); + }) + await subscriptionA.unsubscribe(); + await objectA.save({ foo: 'bar' }); + await objectB.save({ foo: 'baz' }); + + setTimeout(() => { + assert.equal(count, 1); + done(); + }, 1000); + }); +}); diff --git a/integration/test/helper.js b/integration/test/helper.js index c1cb726cc..df4355669 100644 --- a/integration/test/helper.js +++ b/integration/test/helper.js @@ -1,8 +1,13 @@ +const ParseServer = require('parse-server').ParseServer; + jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; beforeAll((done) => { const { app } = require('../server'); - app.listen(1337, () => { + const httpServer = require('http').createServer(app); + + httpServer.listen(1337, () => { console.log('parse-server running on port 1337.'); done(); }); + ParseServer.createLiveQueryServer(httpServer); }); diff --git a/src/CoreManager.js b/src/CoreManager.js index af7d97d6f..055f85ef5 100644 --- a/src/CoreManager.js +++ b/src/CoreManager.js @@ -393,10 +393,9 @@ module.exports = { setLiveQueryController(controller: any) { requireMethods('LiveQueryController', [ - 'subscribe', - 'unsubscribe', - 'open', - 'close', + 'setDefaultLiveQueryClient', + 'getDefaultLiveQueryClient', + '_clearCachedDefaultClient', ], controller); config['LiveQueryController'] = controller; }, diff --git a/src/LiveQueryClient.js b/src/LiveQueryClient.js index c9fdea898..7aa39bc21 100644 --- a/src/LiveQueryClient.js +++ b/src/LiveQueryClient.js @@ -209,10 +209,6 @@ class LiveQueryClient extends EventEmitter { this.socket.send(JSON.stringify(subscribeRequest)); }); - // adding listener so process does not crash - // best practice is for developer to register their own listener - subscription.on('error', () => {}); - return subscription; } diff --git a/src/LiveQuerySubscription.js b/src/LiveQuerySubscription.js index b2013a26b..8ea25d751 100644 --- a/src/LiveQuerySubscription.js +++ b/src/LiveQuerySubscription.js @@ -101,12 +101,16 @@ class Subscription extends EventEmitter { this.id = id; this.query = query; this.sessionToken = sessionToken; + + // adding listener so process does not crash + // best practice is for developer to register their own listener + this.on('error', () => {}); } /** - * closes the subscription + * Close the subscription */ - unsubscribe() { + unsubscribe(): Promise { return CoreManager.getLiveQueryController().getDefaultLiveQueryClient().then((liveQueryClient) => { liveQueryClient.unsubscribe(this); this.emit('close'); diff --git a/src/ParseLiveQuery.js b/src/ParseLiveQuery.js index e00d9a022..ea775ec5a 100644 --- a/src/ParseLiveQuery.js +++ b/src/ParseLiveQuery.js @@ -12,15 +12,10 @@ import EventEmitter from './EventEmitter'; import LiveQueryClient from './LiveQueryClient'; import CoreManager from './CoreManager'; +const url = require('url'); -function open() { - const LiveQueryController = CoreManager.getLiveQueryController(); - LiveQueryController.open(); -} - -function close() { - const LiveQueryController = CoreManager.getLiveQueryController(); - LiveQueryController.close(); +function getLiveQueryClient(): LiveQueryClient { + return CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); } /** @@ -57,10 +52,11 @@ const LiveQuery = new EventEmitter(); /** * After open is called, the LiveQuery will try to send a connect request * to the LiveQuery server. - * - */ -LiveQuery.open = open; +LiveQuery.open = async () => { + const liveQueryClient = await getLiveQueryClient(); + return liveQueryClient.open(); +}; /** * When you're done using LiveQuery, you can call Parse.LiveQuery.close(). @@ -68,147 +64,68 @@ LiveQuery.open = open; * cancel the auto reconnect, and unsubscribe all subscriptions based on it. * If you call query.subscribe() after this, we'll create a new WebSocket * connection to the LiveQuery server. - * - */ -LiveQuery.close = close; +LiveQuery.close = async () => { + const liveQueryClient = await getLiveQueryClient(); + return liveQueryClient.close(); +}; + // Register a default onError callback to make sure we do not crash on error -LiveQuery.on('error', () => { -}); +LiveQuery.on('error', () => {}); export default LiveQuery; -function getSessionToken() { - const controller = CoreManager.getUserController(); - return controller.currentUserAsync().then((currentUser) => { - return currentUser ? currentUser.getSessionToken() : undefined; - }); -} - -function getLiveQueryClient() { - return CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); -} - let defaultLiveQueryClient; + const DefaultLiveQueryController = { - setDefaultLiveQueryClient(liveQueryClient: any) { + setDefaultLiveQueryClient(liveQueryClient: LiveQueryClient) { defaultLiveQueryClient = liveQueryClient; }, - getDefaultLiveQueryClient(): Promise { + + async getDefaultLiveQueryClient(): Promise { if (defaultLiveQueryClient) { - return Promise.resolve(defaultLiveQueryClient); + return defaultLiveQueryClient; + } + const currentUser = await CoreManager.getUserController().currentUserAsync(); + const sessionToken = currentUser ? currentUser.getSessionToken() : undefined; + + let liveQueryServerURL = CoreManager.get('LIVEQUERY_SERVER_URL'); + if (liveQueryServerURL && liveQueryServerURL.indexOf('ws') !== 0) { + throw new Error( + 'You need to set a proper Parse LiveQuery server url before using LiveQueryClient' + ); } - return getSessionToken().then((sessionToken) => { - let liveQueryServerURL = CoreManager.get('LIVEQUERY_SERVER_URL'); - - if (liveQueryServerURL && liveQueryServerURL.indexOf('ws') !== 0) { - throw new Error( - 'You need to set a proper Parse LiveQuery server url before using LiveQueryClient' - ); - } - - // If we can not find Parse.liveQueryServerURL, we try to extract it from Parse.serverURL - if (!liveQueryServerURL) { - const tempServerURL = CoreManager.get('SERVER_URL'); - let protocol = 'ws://'; - // If Parse is being served over SSL/HTTPS, ensure LiveQuery Server uses 'wss://' prefix - if (tempServerURL.indexOf('https') === 0) { - protocol = 'wss://' - } - const host = tempServerURL.replace(/^https?:\/\//, ''); - liveQueryServerURL = protocol + host; - CoreManager.set('LIVEQUERY_SERVER_URL', liveQueryServerURL); - } - - const applicationId = CoreManager.get('APPLICATION_ID'); - const javascriptKey = CoreManager.get('JAVASCRIPT_KEY'); - const masterKey = CoreManager.get('MASTER_KEY'); - // Get currentUser sessionToken if possible - defaultLiveQueryClient = new LiveQueryClient({ - applicationId, - serverURL: liveQueryServerURL, - javascriptKey, - masterKey, - sessionToken, - }); - // Register a default onError callback to make sure we do not crash on error - // Cannot create these events on a nested way because of EventEmiiter from React Native - defaultLiveQueryClient.on('error', (error) => { - LiveQuery.emit('error', error); - }); - defaultLiveQueryClient.on('open', () => { - LiveQuery.emit('open'); - }); - defaultLiveQueryClient.on('close', () => { - LiveQuery.emit('close'); - }); + // If we can not find Parse.liveQueryServerURL, we try to extract it from Parse.serverURL + if (!liveQueryServerURL) { + const serverURL = url.parse(CoreManager.get('SERVER_URL')); + const protocol = serverURL.protocol === 'http:' ? 'ws://' : 'wss://'; + liveQueryServerURL = protocol + serverURL.host + serverURL.pathname; + CoreManager.set('LIVEQUERY_SERVER_URL', liveQueryServerURL); + } - return defaultLiveQueryClient; - }); - }, - open() { - getLiveQueryClient().then((liveQueryClient) => { - return liveQueryClient.open(); + const applicationId = CoreManager.get('APPLICATION_ID'); + const javascriptKey = CoreManager.get('JAVASCRIPT_KEY'); + const masterKey = CoreManager.get('MASTER_KEY'); + + defaultLiveQueryClient = new LiveQueryClient({ + applicationId, + serverURL: liveQueryServerURL, + javascriptKey, + masterKey, + sessionToken, }); - }, - close() { - getLiveQueryClient().then((liveQueryClient) => { - return liveQueryClient.close(); + defaultLiveQueryClient.on('error', (error) => { + LiveQuery.emit('error', error); }); - }, - subscribe(query: any): EventEmitter { - const subscriptionWrap = new EventEmitter(); - - getLiveQueryClient().then((liveQueryClient) => { - if (liveQueryClient.shouldOpen()) { - liveQueryClient.open(); - } - const promiseSessionToken = getSessionToken(); - // new event emitter - return promiseSessionToken.then((sessionToken) => { - - const subscription = liveQueryClient.subscribe(query, sessionToken); - // enter, leave create, etc - - subscriptionWrap.id = subscription.id; - subscriptionWrap.query = subscription.query; - subscriptionWrap.sessionToken = subscription.sessionToken; - subscriptionWrap.unsubscribe = subscription.unsubscribe; - // Cannot create these events on a nested way because of EventEmiiter from React Native - subscription.on('open', () => { - subscriptionWrap.emit('open'); - }); - subscription.on('create', (object) => { - subscriptionWrap.emit('create', object); - }); - subscription.on('update', (object) => { - subscriptionWrap.emit('update', object); - }); - subscription.on('enter', (object) => { - subscriptionWrap.emit('enter', object); - }); - subscription.on('leave', (object) => { - subscriptionWrap.emit('leave', object); - }); - subscription.on('delete', (object) => { - subscriptionWrap.emit('delete', object); - }); - subscription.on('close', (object) => { - subscriptionWrap.emit('close', object); - }); - subscription.on('error', (object) => { - subscriptionWrap.emit('error', object); - }); - }); + defaultLiveQueryClient.on('open', () => { + LiveQuery.emit('open'); }); - return subscriptionWrap; - }, - unsubscribe(subscription: any) { - getLiveQueryClient().then((liveQueryClient) => { - return liveQueryClient.unsubscribe(subscription); + defaultLiveQueryClient.on('close', () => { + LiveQuery.emit('close'); }); + return defaultLiveQueryClient; }, _clearCachedDefaultClient() { defaultLiveQueryClient = null; diff --git a/src/ParseQuery.js b/src/ParseQuery.js index e0ea9cc66..2a3d4003d 100644 --- a/src/ParseQuery.js +++ b/src/ParseQuery.js @@ -17,6 +17,7 @@ import ParseGeoPoint from './ParseGeoPoint'; import ParseObject from './ParseObject'; import OfflineQuery from './OfflineQuery'; +import type LiveQuerySubscription from './LiveQuerySubscription'; import type { RequestOptions, FullOptions } from './RESTController'; type BatchOptions = FullOptions & { batchSize?: number }; @@ -1478,12 +1479,20 @@ class ParseQuery { /** * Subscribe this query to get liveQuery updates - * @return {LiveQuerySubscription} Returns the liveQuerySubscription, it's an event emitter + * + * @return {Promise} Returns the liveQuerySubscription, it's an event emitter * which can be used to get liveQuery updates. */ - subscribe(): any { - const controller = CoreManager.getLiveQueryController(); - return controller.subscribe(this); + async subscribe(): Promise { + const currentUser = await CoreManager.getUserController().currentUserAsync(); + const sessionToken = currentUser ? currentUser.getSessionToken() : undefined; + + const liveQueryClient = await CoreManager.getLiveQueryController().getDefaultLiveQueryClient(); + if (liveQueryClient.shouldOpen()) { + liveQueryClient.open(); + } + const subscription = liveQueryClient.subscribe(this, sessionToken); + return subscription; } /** diff --git a/src/__tests__/ParseLiveQuery-test.js b/src/__tests__/ParseLiveQuery-test.js index 497962575..22b0cac2b 100644 --- a/src/__tests__/ParseLiveQuery-test.js +++ b/src/__tests__/ParseLiveQuery-test.js @@ -17,10 +17,14 @@ jest.dontMock('../EventEmitter'); jest.dontMock('../promiseUtils'); // Forces the loading -require('../ParseLiveQuery'); +const LiveQuery = require('../ParseLiveQuery').default; const CoreManager = require('../CoreManager'); const ParseQuery = require('../ParseQuery').default; const LiveQuerySubscription = require('../LiveQuerySubscription').default; +const mockLiveQueryClient = { + open: jest.fn(), + close: jest.fn(), +}; describe('ParseLiveQuery', () => { beforeEach(() => { @@ -63,7 +67,7 @@ describe('ParseLiveQuery', () => { }); }); - it('automatically generates a websocket url', (done) => { + it('automatically generates a ws websocket url', (done) => { CoreManager.set('UserController', { currentUserAsync() { return Promise.resolve(undefined); @@ -71,6 +75,27 @@ describe('ParseLiveQuery', () => { }); CoreManager.set('APPLICATION_ID', 'appid'); CoreManager.set('JAVASCRIPT_KEY', 'jskey'); + CoreManager.set('SERVER_URL', 'http://api.parse.com/1'); + CoreManager.set('LIVEQUERY_SERVER_URL', null); + const controller = CoreManager.getLiveQueryController(); + controller.getDefaultLiveQueryClient().then((client) => { + expect(client.serverURL).toBe('ws://api.parse.com/1'); + expect(client.applicationId).toBe('appid'); + expect(client.javascriptKey).toBe('jskey'); + expect(client.sessionToken).toBe(undefined); + done(); + }); + }); + + it('automatically generates a wss websocket url', (done) => { + CoreManager.set('UserController', { + currentUserAsync() { + return Promise.resolve(undefined); + } + }); + CoreManager.set('APPLICATION_ID', 'appid'); + CoreManager.set('JAVASCRIPT_KEY', 'jskey'); + CoreManager.set('SERVER_URL', 'https://api.parse.com/1'); CoreManager.set('LIVEQUERY_SERVER_URL', null); const controller = CoreManager.getLiveQueryController(); controller.getDefaultLiveQueryClient().then((client) => { @@ -105,6 +130,32 @@ describe('ParseLiveQuery', () => { }); }); + it('handle LiveQueryClient events', async () => { + const spy = jest.spyOn(LiveQuery, 'emit'); + + CoreManager.set('UserController', { + currentUserAsync() { + return Promise.resolve({ + getSessionToken() { + return 'token'; + } + }); + } + }); + CoreManager.set('APPLICATION_ID', 'appid'); + CoreManager.set('JAVASCRIPT_KEY', 'jskey'); + CoreManager.set('LIVEQUERY_SERVER_URL', null); + const controller = CoreManager.getLiveQueryController(); + const client = await controller.getDefaultLiveQueryClient(); + client.emit('error', 'error thrown'); + client.emit('open'); + client.emit('close'); + expect(spy.mock.calls[0]).toEqual(['error', 'error thrown']); + expect(spy.mock.calls[1]).toEqual(['open']); + expect(spy.mock.calls[2]).toEqual(['close']); + spy.mockRestore(); + }); + it('subscribes to all subscription events', (done) => { CoreManager.set('UserController', { @@ -122,11 +173,11 @@ describe('ParseLiveQuery', () => { const controller = CoreManager.getLiveQueryController(); - controller.getDefaultLiveQueryClient().then((client) => { + controller.getDefaultLiveQueryClient().then(async (client) => { const query = new ParseQuery("ObjectType"); query.equalTo("test", "value"); - const ourSubscription = controller.subscribe(query, "close"); + const ourSubscription = await client.subscribe(query, "close"); const isCalled = {}; ["open", @@ -142,7 +193,7 @@ describe('ParseLiveQuery', () => { }); }); - // controller.subscribe() completes asynchronously, + // client.subscribe() completes asynchronously, // so we need to give it a chance to complete before finishing setTimeout(() => { try { @@ -190,4 +241,32 @@ describe('ParseLiveQuery', () => { const subscription = new LiveQuerySubscription('0', query, 'token'); subscription.unsubscribe().then(done).catch(done.fail); }); + + it('can handle LiveQuery open event', async () => { + jest.spyOn(mockLiveQueryClient, 'open'); + const controller = CoreManager.getLiveQueryController(); + controller.setDefaultLiveQueryClient(mockLiveQueryClient); + + await LiveQuery.open(); + expect(mockLiveQueryClient.open).toHaveBeenCalled(); + }); + + it('can handle LiveQuery close event', async () => { + jest.spyOn(mockLiveQueryClient, 'close'); + const controller = CoreManager.getLiveQueryController(); + controller.setDefaultLiveQueryClient(mockLiveQueryClient); + + await LiveQuery.close(); + expect(mockLiveQueryClient.close).toHaveBeenCalled(); + }); + + it('can handle LiveQuery error event', async () => { + try { + LiveQuery.emit('error'); + expect(true).toBe(true); + } catch (error) { + // Should not throw error + expect(false).toBe(true); + } + }); }); diff --git a/src/__tests__/ParseQuery-test.js b/src/__tests__/ParseQuery-test.js index 347bd8755..194a38402 100644 --- a/src/__tests__/ParseQuery-test.js +++ b/src/__tests__/ParseQuery-test.js @@ -19,6 +19,7 @@ jest.dontMock('../UniqueInstanceStateController'); jest.dontMock('../ObjectStateMutations'); jest.dontMock('../LocalDatastore'); jest.dontMock('../OfflineQuery'); +jest.dontMock('../LiveQuerySubscription'); const mockObject = function(className) { this.className = className; @@ -49,6 +50,7 @@ const ParseError = require('../ParseError').default; const ParseGeoPoint = require('../ParseGeoPoint').default; let ParseObject = require('../ParseObject'); let ParseQuery = require('../ParseQuery').default; +const LiveQuerySubscription = require('../LiveQuerySubscription').default; describe('ParseQuery', () => { it('can be constructed from a class name', () => { @@ -2627,4 +2629,92 @@ describe('ParseQuery LocalDatastore', () => { const results = await q.find(); expect(results[0].get('foo')).toEqual('baz'); }); + + it('can subscribe to query if client is already open', async () => { + const mockLiveQueryClient = { + shouldOpen: function() { + return false; + }, + subscribe: function(query, sessionToken) { + return new LiveQuerySubscription('0', query, sessionToken); + }, + }; + CoreManager.set('UserController', { + currentUserAsync() { + return Promise.resolve({ + getSessionToken() { + return 'token'; + } + }); + } + }); + CoreManager.set('LiveQueryController', { + getDefaultLiveQueryClient() { + return Promise.resolve(mockLiveQueryClient); + } + }); + const query = new ParseQuery('TestObject'); + const subscription = await query.subscribe(); + expect(subscription.id).toBe('0'); + expect(subscription.sessionToken).toBe('token'); + expect(subscription.query).toEqual(query); + }); + + it('can subscribe to query if client is not open', async () => { + const mockLiveQueryClient = { + shouldOpen: function() { + return true; + }, + open: function() {}, + subscribe: function(query, sessionToken) { + return new LiveQuerySubscription('0', query, sessionToken); + }, + }; + CoreManager.set('UserController', { + currentUserAsync() { + return Promise.resolve({ + getSessionToken() { + return 'token'; + } + }); + } + }); + CoreManager.set('LiveQueryController', { + getDefaultLiveQueryClient() { + return Promise.resolve(mockLiveQueryClient); + } + }); + const query = new ParseQuery('TestObject'); + const subscription = await query.subscribe(); + expect(subscription.id).toBe('0'); + expect(subscription.sessionToken).toBe('token'); + expect(subscription.query).toEqual(query); + }); + + it('can subscribe to query without sessionToken', async () => { + const mockLiveQueryClient = { + shouldOpen: function() { + return true; + }, + open: function() {}, + subscribe: function(query, sessionToken) { + return new LiveQuerySubscription('0', query, sessionToken); + }, + }; + CoreManager.set('UserController', { + currentUserAsync() { + return Promise.resolve(null); + } + }); + CoreManager.set('LiveQueryController', { + getDefaultLiveQueryClient() { + return Promise.resolve(mockLiveQueryClient); + } + }); + const query = new ParseQuery('TestObject'); + const subscription = await query.subscribe(); + expect(subscription.id).toBe('0'); + expect(subscription.sessionToken).toBeUndefined(); + expect(subscription.query).toEqual(query); + }); });