Files
kami-parse-server/spec/ParseLiveQueryServer.spec.js
2024-07-16 10:34:10 +02:00

2069 lines
69 KiB
JavaScript

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', () => {
beforeEach(() => reconfigureServer({ appId: 'mongo_init_test' }));
it('can be initialized through ParseServer without liveQueryServerOptions', async () => {
const parseServer = await ParseServer.startApp({
appId: 'hello',
masterKey: 'world',
port: 22345,
mountPath: '/1',
serverURL: 'http://localhost:12345/1',
liveQuery: {
classNames: ['Yolo'],
},
startLiveQueryServer: true,
});
expect(parseServer.liveQueryServer).not.toBeUndefined();
expect(parseServer.liveQueryServer.server).toBe(parseServer.server);
await new Promise(resolve => parseServer.server.close(resolve));
});
it('can be initialized through ParseServer with liveQueryServerOptions', async () => {
const parseServer = await ParseServer.startApp({
appId: 'hello',
masterKey: 'world',
port: 22346,
mountPath: '/1',
serverURL: 'http://localhost:12345/1',
liveQuery: {
classNames: ['Yolo'],
},
liveQueryServerOptions: {
port: 22347,
},
});
expect(parseServer.liveQueryServer).not.toBeUndefined();
expect(parseServer.liveQueryServer.server).not.toBe(parseServer.server);
await new Promise(resolve => parseServer.server.close(resolve));
});
});
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',
},
keys: ['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',
},
keys: ['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].keys).toBe(query.keys);
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',
},
keys: ['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',
},
keys: ['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].keys).toBe(queryAgain.keys);
});
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('sends correct object for dates', async () => {
jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'matchesQuery');
const parseLiveQueryServer = new ParseLiveQueryServer({});
const date = new Date();
const message = {
currentParseObject: {
date: { __type: 'Date', iso: date.toISOString() },
__type: 'Object',
key: 'value',
className: testClassName,
},
};
// Add mock client
const clientId = 1;
const client = addMockClient(parseLiveQueryServer, clientId);
const requestId2 = 2;
await addMockSubscription(parseLiveQueryServer, clientId, requestId2);
parseLiveQueryServer._matchesACL = function () {
return Promise.resolve(true);
};
parseLiveQueryServer._inflateParseObject(message);
parseLiveQueryServer._onAfterSave(message);
// Make sure we send leave and enter command to client
await timeout();
expect(client.pushCreate).toHaveBeenCalledWith(
requestId2,
{
className: 'TestObject',
key: 'value',
date: { __type: 'Date', iso: date.toISOString() },
},
null
);
});
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 keys', 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',
},
keys: ['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 handle create command with watch', async () => {
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',
},
watch: ['yolo'],
};
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).not.toHaveBeenCalled();
message.currentParseObject.set('yolo', 'test');
parseLiveQueryServer._onAfterSave(message);
await timeout();
const args = parseWebSocket.send.calls.mostRecent().args;
const toSend = JSON.parse(args[0]);
expect(toSend.object).toBeDefined();
expect(toSend.original).toBeUndefined();
});
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: 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',
},
keys: ['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]]]);
}
subscription.query = query.where;
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,
});
});
});