Add LiveQuery

This commit is contained in:
wangmengyan95
2016-03-10 14:27:00 -08:00
parent cf3606246f
commit 555e25bf33
33 changed files with 3580 additions and 48 deletions

290
spec/Client.spec.js Normal file
View File

@@ -0,0 +1,290 @@
var Client = require('../src/LiveQuery/Client').Client;
var ParseWebSocket = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocket;
describe('Client', function() {
it('can be initialized', function() {
var parseWebSocket = new ParseWebSocket({});
var client = new Client(1, parseWebSocket);
expect(client.id).toBe(1);
expect(client.parseWebSocket).toBe(parseWebSocket);
expect(client.subscriptionInfos.size).toBe(0);
});
it('can push response', function() {
var parseWebSocket = {
send: jasmine.createSpy('send')
};
Client.pushResponse(parseWebSocket, 'message');
expect(parseWebSocket.send).toHaveBeenCalledWith('message');
});
it('can push error', function() {
var parseWebSocket = {
send: jasmine.createSpy('send')
};
Client.pushError(parseWebSocket, 1, 'error', true);
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('error');
expect(messageJSON.error).toBe('error');
expect(messageJSON.code).toBe(1);
expect(messageJSON.reconnect).toBe(true);
});
it('can add subscription information', function() {
var subscription = {};
var fields = ['test'];
var subscriptionInfo = {
subscription: subscription,
fields: fields
}
var client = new Client(1, {});
client.addSubscriptionInfo(1, subscriptionInfo);
expect(client.subscriptionInfos.size).toBe(1);
expect(client.subscriptionInfos.get(1)).toBe(subscriptionInfo);
});
it('can get subscription information', function() {
var subscription = {};
var fields = ['test'];
var subscriptionInfo = {
subscription: subscription,
fields: fields
}
var client = new Client(1, {});
client.addSubscriptionInfo(1, subscriptionInfo);
var subscriptionInfoAgain = client.getSubscriptionInfo(1);
expect(subscriptionInfoAgain).toBe(subscriptionInfo);
});
it('can delete subscription information', function() {
var subscription = {};
var fields = ['test'];
var subscriptionInfo = {
subscription: subscription,
fields: fields
}
var client = new Client(1, {});
client.addSubscriptionInfo(1, subscriptionInfo);
client.deleteSubscriptionInfo(1);
expect(client.subscriptionInfos.size).toBe(0);
});
it('can generate ParseObject JSON with null selected field', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
};
var client = new Client(1, {});
expect(client._toJSONWithFields(parseObjectJSON, null)).toBe(parseObjectJSON);
});
it('can generate ParseObject JSON with undefined selected field', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
};
var client = new Client(1, {});
expect(client._toJSONWithFields(parseObjectJSON, undefined)).toBe(parseObjectJSON);
});
it('can generate ParseObject JSON with selected fields', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
test: 'test'
};
var client = new Client(1, {});
expect(client._toJSONWithFields(parseObjectJSON, ['test'])).toEqual({
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
test: 'test'
});
});
it('can generate ParseObject JSON with nonexistent selected fields', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
test: 'test'
};
var client = new Client(1, {});
var limitedParseObject = client._toJSONWithFields(parseObjectJSON, ['name']);
expect(limitedParseObject).toEqual({
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
});
expect('name' in limitedParseObject).toBe(false);
});
it('can push connect response', function() {
var parseWebSocket = {
send: jasmine.createSpy('send')
};
var client = new Client(1, parseWebSocket);
client.pushConnect();
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('connected');
expect(messageJSON.clientId).toBe(1);
});
it('can push subscribe response', function() {
var parseWebSocket = {
send: jasmine.createSpy('send')
};
var client = new Client(1, parseWebSocket);
client.pushSubscribe(2);
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('subscribed');
expect(messageJSON.clientId).toBe(1);
expect(messageJSON.requestId).toBe(2);
});
it('can push unsubscribe response', function() {
var parseWebSocket = {
send: jasmine.createSpy('send')
};
var client = new Client(1, parseWebSocket);
client.pushUnsubscribe(2);
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('unsubscribed');
expect(messageJSON.clientId).toBe(1);
expect(messageJSON.requestId).toBe(2);
});
it('can push create response', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
test: 'test'
};
var parseWebSocket = {
send: jasmine.createSpy('send')
};
var client = new Client(1, parseWebSocket);
client.pushCreate(2, parseObjectJSON);
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('create');
expect(messageJSON.clientId).toBe(1);
expect(messageJSON.requestId).toBe(2);
expect(messageJSON.object).toEqual(parseObjectJSON);
});
it('can push enter response', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
test: 'test'
};
var parseWebSocket = {
send: jasmine.createSpy('send')
};
var client = new Client(1, parseWebSocket);
client.pushEnter(2, parseObjectJSON);
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('enter');
expect(messageJSON.clientId).toBe(1);
expect(messageJSON.requestId).toBe(2);
expect(messageJSON.object).toEqual(parseObjectJSON);
});
it('can push update response', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
test: 'test'
};
var parseWebSocket = {
send: jasmine.createSpy('send')
};
var client = new Client(1, parseWebSocket);
client.pushUpdate(2, parseObjectJSON);
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('update');
expect(messageJSON.clientId).toBe(1);
expect(messageJSON.requestId).toBe(2);
expect(messageJSON.object).toEqual(parseObjectJSON);
});
it('can push leave response', function() {
var parseObjectJSON = {
key : 'value',
className: 'test',
objectId: 'test',
updatedAt: '2015-12-07T21:27:13.746Z',
createdAt: '2015-12-07T21:27:13.746Z',
ACL: 'test',
test: 'test'
};
var parseWebSocket = {
send: jasmine.createSpy('send')
};
var client = new Client(1, parseWebSocket);
client.pushLeave(2, parseObjectJSON);
var lastCall = parseWebSocket.send.calls.first();
var messageJSON = JSON.parse(lastCall.args[0]);
expect(messageJSON.op).toBe('leave');
expect(messageJSON.clientId).toBe(1);
expect(messageJSON.requestId).toBe(2);
expect(messageJSON.object).toEqual(parseObjectJSON);
});
});

View File

@@ -0,0 +1,44 @@
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
describe('EventEmitterPubSub', function() {
it('can publish and subscribe', function() {
var publisher = EventEmitterPubSub.createPublisher();
var subscriber = EventEmitterPubSub.createSubscriber();
subscriber.subscribe('testChannel');
// Register mock checked for subscriber
var isChecked = false;
subscriber.on('message', function(channel, message) {
isChecked = true;
expect(channel).toBe('testChannel');
expect(message).toBe('testMessage');
});
publisher.publish('testChannel', 'testMessage');
// Make sure the callback is checked
expect(isChecked).toBe(true);
});
it('can unsubscribe', function() {
var publisher = EventEmitterPubSub.createPublisher();
var subscriber = EventEmitterPubSub.createSubscriber();
subscriber.subscribe('testChannel');
subscriber.unsubscribe('testChannel');
// Register mock checked for subscriber
var isCalled = false;
subscriber.on('message', function(channel, message) {
isCalled = true;
});
publisher.publish('testChannel', 'testMessage');
// Make sure the callback is not called
expect(isCalled).toBe(false);
});
it('can unsubscribe not subscribing channel', function() {
var subscriber = EventEmitterPubSub.createSubscriber();
// Make sure subscriber does not throw exception
subscriber.unsubscribe('testChannel');
});
});

View File

@@ -0,0 +1,70 @@
var ParseCloudCodePublisher = require('../src/LiveQuery/ParseCloudCodePublisher').ParseCloudCodePublisher;
var Parse = require('parse/node');
describe('ParseCloudCodePublisher', function() {
beforeEach(function(done) {
// Mock ParsePubSub
var mockParsePubSub = {
createPublisher: jasmine.createSpy('publish').and.returnValue({
publish: jasmine.createSpy('publish'),
on: jasmine.createSpy('on')
}),
createSubscriber: jasmine.createSpy('publish').and.returnValue({
subscribe: jasmine.createSpy('subscribe'),
on: jasmine.createSpy('on')
})
};
jasmine.mockLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub', mockParsePubSub);
done();
});
it('can initialize', function() {
var config = {}
var publisher = new ParseCloudCodePublisher(config);
var ParsePubSub = require('../src/LiveQuery/ParsePubSub').ParsePubSub;
expect(ParsePubSub.createPublisher).toHaveBeenCalledWith(config);
});
it('can handle cloud code afterSave request', function() {
var publisher = new ParseCloudCodePublisher({});
publisher._onCloudCodeMessage = jasmine.createSpy('onCloudCodeMessage');
var request = {};
publisher.onCloudCodeAfterSave(request);
expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith('afterSave', request);
});
it('can handle cloud code afterDelete request', function() {
var publisher = new ParseCloudCodePublisher({});
publisher._onCloudCodeMessage = jasmine.createSpy('onCloudCodeMessage');
var request = {};
publisher.onCloudCodeAfterDelete(request);
expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith('afterDelete', request);
});
it('can handle cloud code request', function() {
var publisher = new ParseCloudCodePublisher({});
var currentParseObject = new Parse.Object('Test');
currentParseObject.set('key', 'value');
var originalParseObject = new Parse.Object('Test');
originalParseObject.set('key', 'originalValue');
var request = {
object: currentParseObject,
original: originalParseObject
};
publisher._onCloudCodeMessage('afterSave', request);
var args = publisher.parsePublisher.publish.calls.mostRecent().args;
expect(args[0]).toBe('afterSave');
var message = JSON.parse(args[1]);
expect(message.currentParseObject).toEqual(request.object._toFullJSON());
expect(message.originalParseObject).toEqual(request.original._toFullJSON());
});
afterEach(function(){
jasmine.restoreLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub');
});
});

View File

@@ -0,0 +1,962 @@
var Parse = require('parse/node');
var ParseLiveQueryServer = require('../src/LiveQuery/ParseLiveQueryServer').ParseLiveQueryServer;
// Global mock info
var queryHashValue = 'hash';
var testUserId = 'userId';
var testClassName = 'TestObject';
describe('ParseLiveQueryServer', function() {
beforeEach(function(done) {
// Mock ParseWebSocketServer
var mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer');
jasmine.mockLibrary('../src/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer', mockParseWebSocketServer);
// Mock Client
var mockClient = function() {
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');
}
mockClient.pushError = jasmine.createSpy('pushError');
jasmine.mockLibrary('../src/LiveQuery/Client', 'Client', mockClient);
// Mock Subscription
var mockSubscriotion = function() {
this.addClientSubscription = jasmine.createSpy('addClientSubscription');
this.deleteClientSubscription = jasmine.createSpy('deleteClientSubscription');
}
jasmine.mockLibrary('../src/LiveQuery/Subscription', 'Subscription', mockSubscriotion);
// Mock queryHash
var mockQueryHash = jasmine.createSpy('matchesQuery').and.returnValue(queryHashValue);
jasmine.mockLibrary('../src/LiveQuery/QueryTools', 'queryHash', mockQueryHash);
// Mock matchesQuery
var mockMatchesQuery = jasmine.createSpy('matchesQuery').and.returnValue(true);
jasmine.mockLibrary('../src/LiveQuery/QueryTools', 'matchesQuery', mockMatchesQuery);
// Mock tv4
var mockValidate = function() {
return true;
}
jasmine.mockLibrary('tv4', 'validate', mockValidate);
// Mock ParsePubSub
var 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('../src/LiveQuery/ParsePubSub', 'ParsePubSub', mockParsePubSub);
// Make mock SessionTokenCache
var mockSessionTokenCache = function(){
this.getUserId = function(sessionToken){
if (typeof sessionToken === 'undefined') {
return Parse.Promise.as(undefined);
}
if (sessionToken === null) {
return Parse.Promise.error();
}
return Parse.Promise.as(testUserId);
};
};
jasmine.mockLibrary('../src/LiveQuery/SessionTokenCache', 'SessionTokenCache', mockSessionTokenCache);
done();
});
it('can be initialized', function() {
var httpServer = {};
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, httpServer);
expect(parseLiveQueryServer.clientId).toBe(0);
expect(parseLiveQueryServer.clients.size).toBe(0);
expect(parseLiveQueryServer.subscriptions.size).toBe(0);
});
it('can handle connect command', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var parseWebSocket = {
clientId: -1
};
parseLiveQueryServer._validateKeys = jasmine.createSpy('validateKeys').and.returnValue(true);
parseLiveQueryServer._handleConnect(parseWebSocket);
expect(parseLiveQueryServer.clientId).toBe(1);
expect(parseWebSocket.clientId).toBe(0);
var client = parseLiveQueryServer.clients.get(0);
expect(client).not.toBeNull();
// Make sure we send connect response to the client
expect(client.pushConnect).toHaveBeenCalled();
});
it('can handle subscribe command without clientId', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var incompleteParseConn = {
};
parseLiveQueryServer._handleSubscribe(incompleteParseConn, {});
var Client = require('../src/LiveQuery/Client').Client;
expect(Client.pushError).toHaveBeenCalled();
});
it('can handle subscribe command with new query', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Handle mock subscription
var parseWebSocket = {
clientId: clientId
};
var query = {
className: 'test',
where: {
key: 'value'
},
fields: [ 'test' ]
}
var requestId = 2;
var request = {
query: query,
requestId: requestId,
sessionToken: 'sessionToken'
}
parseLiveQueryServer._handleSubscribe(parseWebSocket, request);
// Make sure we add the subscription to the server
var subscriptions = parseLiveQueryServer.subscriptions;
expect(subscriptions.size).toBe(1);
expect(subscriptions.get(query.className)).not.toBeNull();
var 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
var subscription = classSubscriptions.get('hash');
expect(subscription.addClientSubscription).toHaveBeenCalledWith(clientId, requestId);
// Make sure we add subscriptionInfo to the client
var 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', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Add two mock clients
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
var clientIdAgain = 2;
var clientAgain = addMockClient(parseLiveQueryServer, clientIdAgain);
// Add subscription for mock client 1
var parseWebSocket = {
clientId: clientId
};
var requestId = 2;
var query = {
className: 'test',
where: {
key: 'value'
},
fields: [ 'test' ]
}
addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query);
// Add subscription for mock client 2
var parseWebSocketAgain = {
clientId: clientIdAgain
};
var queryAgain = {
className: 'test',
where: {
key: 'value'
},
fields: [ 'testAgain' ]
}
var requestIdAgain = 1;
addMockSubscription(parseLiveQueryServer, clientIdAgain, requestIdAgain, parseWebSocketAgain, queryAgain);
// Make sure we only have one subscription
var subscriptions = parseLiveQueryServer.subscriptions;
expect(subscriptions.size).toBe(1);
expect(subscriptions.get(query.className)).not.toBeNull();
var classSubscriptions = subscriptions.get(query.className);
expect(classSubscriptions.size).toBe(1);
expect(classSubscriptions.get('hash')).not.toBeNull();
// Make sure we add clientInfo to the subscription
var subscription = classSubscriptions.get('hash');
// Make sure client 2 info has been added
var 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() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var incompleteParseConn = {
};
parseLiveQueryServer._handleUnsubscribe(incompleteParseConn, {});
var Client = require('../src/LiveQuery/Client').Client;
expect(Client.pushError).toHaveBeenCalled();
});
it('can handle unsubscribe command without not existed client', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var parseWebSocket = {
clientId: 1
};
parseLiveQueryServer._handleUnsubscribe(parseWebSocket, {});
var Client = require('../src/LiveQuery/Client').Client;
expect(Client.pushError).toHaveBeenCalled();
});
it('can handle unsubscribe command without not existed query', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Handle unsubscribe command
var parseWebSocket = {
clientId: 1
};
parseLiveQueryServer._handleUnsubscribe(parseWebSocket, {});
var Client = require('../src/LiveQuery/Client').Client;
expect(Client.pushError).toHaveBeenCalled();
});
it('can handle unsubscribe command', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Add subscription for mock client
var parseWebSocket = {
clientId: 1
};
var requestId = 2;
var subscription = addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket);
// Mock client.getSubscriptionInfo
var subscriptionInfo = client.addSubscriptionInfo.calls.mostRecent().args[1];
client.getSubscriptionInfo = function() {
return subscriptionInfo;
};
// Handle unsubscribe command
var 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
var subscriptions = parseLiveQueryServer.subscriptions;
expect(subscriptions.size).toBe(0);
});
it('can set connect command message handler for a parseWebSocket', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Register mock connect/subscribe/unsubscribe handler for the server
parseLiveQueryServer._handleConnect = jasmine.createSpy('_handleSubscribe');
// Make mock parseWebsocket
var EventEmitter = require('events');
var parseWebSocket = new EventEmitter();
// Register message handlers for the parseWebSocket
parseLiveQueryServer._onConnect(parseWebSocket);
// Check connect request
var connectRequest = {
op: 'connect'
};
// Trigger message event
parseWebSocket.emit('message', connectRequest);
// Make sure _handleConnect is called
var args = parseLiveQueryServer._handleConnect.calls.mostRecent().args;
expect(args[0]).toBe(parseWebSocket);
});
it('can set subscribe command message handler for a parseWebSocket', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Register mock connect/subscribe/unsubscribe handler for the server
parseLiveQueryServer._handleSubscribe = jasmine.createSpy('_handleSubscribe');
// Make mock parseWebsocket
var EventEmitter = require('events');
var parseWebSocket = new EventEmitter();
// Register message handlers for the parseWebSocket
parseLiveQueryServer._onConnect(parseWebSocket);
// Check subscribe request
var subscribeRequest = '{"op":"subscribe"}';
// Trigger message event
parseWebSocket.emit('message', subscribeRequest);
// Make sure _handleSubscribe is called
var 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() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Register mock connect/subscribe/unsubscribe handler for the server
parseLiveQueryServer._handleUnsubscribe = jasmine.createSpy('_handleSubscribe');
// Make mock parseWebsocket
var EventEmitter = require('events');
var parseWebSocket = new EventEmitter();
// Register message handlers for the parseWebSocket
parseLiveQueryServer._onConnect(parseWebSocket);
// Check unsubscribe request
var unsubscribeRequest = '{"op":"unsubscribe"}';
// Trigger message event
parseWebSocket.emit('message', unsubscribeRequest);
// Make sure _handleUnsubscribe is called
var args = parseLiveQueryServer._handleUnsubscribe.calls.mostRecent().args;
expect(args[0]).toBe(parseWebSocket);
expect(JSON.stringify(args[1])).toBe(unsubscribeRequest);
});
it('can set unknown command message handler for a parseWebSocket', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock parseWebsocket
var EventEmitter = require('events');
var parseWebSocket = new EventEmitter();
// Register message handlers for the parseWebSocket
parseLiveQueryServer._onConnect(parseWebSocket);
// Check unknown request
var unknownRequest = '{"op":"unknown"}';
// Trigger message event
parseWebSocket.emit('message', unknownRequest);
var Client = require('../src/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() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var EventEmitter = require('events');
var 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');
});
// TODO: Test server can set disconnect command message handler for a parseWebSocket
it('has no subscription and can handle object delete command', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make deletedParseObject
var parseObject = new Parse.Object(testClassName);
parseObject._finishFetch({
key: 'value',
className: testClassName
});
// Make mock message
var 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', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make deletedParseObject
var parseObject = new Parse.Object(testClassName);
parseObject._finishFetch({
key: 'value',
className: testClassName
});
// Make mock message
var message = {
currentParseObject: parseObject
};
// Add mock client
var clientId = 1;
addMockClient(parseLiveQueryServer, clientId);
// Add mock subscription
var requestId = 2;
addMockSubscription(parseLiveQueryServer, clientId, requestId);
var 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', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make deletedParseObject
var parseObject = new Parse.Object(testClassName);
parseObject._finishFetch({
key: 'value',
className: testClassName
});
// Make mock message
var message = {
currentParseObject: parseObject
};
// Add mock client
var clientId = 1;
addMockClient(parseLiveQueryServer, clientId);
// Add mock subscription
var requestId = 2;
addMockSubscription(parseLiveQueryServer, clientId, requestId);
var client = parseLiveQueryServer.clients.get(clientId);
// Mock _matchesSubscription to return matching
parseLiveQueryServer._matchesSubscription = function() {
return true;
};
parseLiveQueryServer._matchesACL = function() {
return Parse.Promise.as(true);
};
parseLiveQueryServer._onAfterDelete(message);
// Make sure we send command to client, since _matchesACL is async, we have to
// wait and check
setTimeout(function() {
expect(client.pushDelete).toHaveBeenCalled();
done();
}, jasmine.ASYNC_TEST_WAIT_TIME);
});
it('has no subscription and can handle object save command', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock request message
var 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', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock request message
var message = generateMockMessage();
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Add mock subscription
var requestId = 2;
addMockSubscription(parseLiveQueryServer, clientId, requestId);
// Mock _matchesSubscription to return not matching
parseLiveQueryServer._matchesSubscription = function() {
return false;
};
parseLiveQueryServer._matchesACL = function() {
return Parse.Promise.as(true)
};
// Trigger onAfterSave
parseLiveQueryServer._onAfterSave(message);
// Make sure we do not send command to client
setTimeout(function(){
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();
}, jasmine.ASYNC_TEST_WAIT_TIME);
});
it('can handle object enter command which matches some subscriptions', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock request message
var message = generateMockMessage(true);
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Add mock subscription
var requestId = 2;
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
var counter = 0;
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
if (!parseObject) {
return false;
}
counter += 1;
return counter % 2 === 0;
};
parseLiveQueryServer._matchesACL = function() {
return Parse.Promise.as(true)
};
parseLiveQueryServer._onAfterSave(message);
// Make sure we send enter command to client
setTimeout(function(){
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();
}, jasmine.ASYNC_TEST_WAIT_TIME);
});
it('can handle object update command which matches some subscriptions', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock request message
var message = generateMockMessage(true);
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Add mock subscription
var requestId = 2;
addMockSubscription(parseLiveQueryServer, clientId, requestId);
// Mock _matchesSubscription to return matching
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
if (!parseObject) {
return false;
}
return true;
};
parseLiveQueryServer._matchesACL = function() {
return Parse.Promise.as(true)
};
parseLiveQueryServer._onAfterSave(message);
// Make sure we send update command to client
setTimeout(function(){
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();
}, jasmine.ASYNC_TEST_WAIT_TIME);
});
it('can handle object leave command which matches some subscriptions', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock request message
var message = generateMockMessage(true);
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Add mock subscription
var requestId = 2;
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
var counter = 0;
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
if (!parseObject) {
return false;
}
counter += 1;
return counter % 2 !== 0;
};
parseLiveQueryServer._matchesACL = function() {
return Parse.Promise.as(true)
};
parseLiveQueryServer._onAfterSave(message);
// Make sure we send leave command to client
setTimeout(function(){
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();
}, jasmine.ASYNC_TEST_WAIT_TIME);
});
it('can handle object create command which matches some subscriptions', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock request message
var message = generateMockMessage();
// Add mock client
var clientId = 1;
var client = addMockClient(parseLiveQueryServer, clientId);
// Add mock subscription
var requestId = 2;
addMockSubscription(parseLiveQueryServer, clientId, requestId);
// Mock _matchesSubscription to return matching
parseLiveQueryServer._matchesSubscription = function(parseObject, subscription){
if (!parseObject) {
return false;
}
return true;
};
parseLiveQueryServer._matchesACL = function() {
return Parse.Promise.as(true)
};
parseLiveQueryServer._onAfterSave(message);
// Make sure we send create command to client
setTimeout(function(){
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();
}, jasmine.ASYNC_TEST_WAIT_TIME);
});
it('can match subscription for null or undefined parse object', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock subscription
var 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() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock subscription
var subscription = {
query: {}
}
var parseObject = {};
expect(parseLiveQueryServer._matchesSubscription(parseObject, subscription)).toBe(true);
// Make sure matchesQuery is called
var matchesQuery = require('../src/LiveQuery/QueryTools').matchesQuery;
expect(matchesQuery).toHaveBeenCalledWith(parseObject, subscription.query);
});
it('can inflate parse object', function() {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
// Make mock request
var objectJSON = {
"className":"testClassName",
"createdAt":"2015-12-22T01:51:12.955Z",
"key":"value",
"objectId":"BfwxBCz6yW",
"updatedAt":"2016-01-05T00:46:45.659Z"
};
var originalObjectJSON = {
"className":"testClassName",
"createdAt":"2015-12-22T01:51:12.955Z",
"key":"originalValue",
"objectId":"BfwxBCz6yW",
"updatedAt":"2016-01-05T00:46:45.659Z"
};
var message = {
currentParseObject: objectJSON,
originalParseObject: originalObjectJSON
};
// Inflate the object
parseLiveQueryServer._inflateParseObject(message);
// Verify object
var 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
var 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 match undefined ACL', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var client = {};
var 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) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue(undefined)
};
var requestId = 0;
var isChecked = false;
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(false);
done();
});
});
it('can match ACL with public read access', function(done) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setPublicReadAccess(true);
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: 'sessionToken'
})
};
var 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) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setReadAccess(testUserId, true);
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: 'sessionToken'
})
};
var 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) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setReadAccess(testUserId, true);
// Mock sessionTokenCache will return false when sessionToken is undefined
var client = {
sessionToken: 'sessionToken',
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: undefined
})
};
var 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) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setReadAccess(testUserId, true);
// Mock sessionTokenCache will return false when sessionToken is undefined
var client = {
sessionToken: undefined,
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: undefined
})
};
var 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) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var 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
var client = {
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: null
})
};
var 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) {
var parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {});
var acl = new Parse.ACL();
acl.setReadAccess(testUserId, true);
// Mock sessionTokenCache will return error when sessionToken is null
var client = {
sessionToken: null,
getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({
sessionToken: null
})
};
var requestId = 0;
parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) {
expect(isMatched).toBe(false);
done();
});
});
it('can validate key when valid key is provided', function() {
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
keyPairs: {
clientKey: 'test'
}
});
var request = {
clientKey: 'test'
}
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).toBeTruthy();
});
it('can validate key when invalid key is provided', function() {
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
keyPairs: {
clientKey: 'test'
}
});
var request = {
clientKey: 'error'
}
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).not.toBeTruthy();
});
it('can validate key when key is not provided', function() {
var parseLiveQueryServer = new ParseLiveQueryServer({}, {
keyPairs: {
clientKey: 'test'
}
});
var request = {
}
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).not.toBeTruthy();
});
it('can validate key when validKerPairs is empty', function() {
var parseLiveQueryServer = new ParseLiveQueryServer({}, {});
var request = {
}
expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).toBeTruthy();
});
afterEach(function(){
jasmine.restoreLibrary('../src/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer');
jasmine.restoreLibrary('../src/LiveQuery/Client', 'Client');
jasmine.restoreLibrary('../src/LiveQuery/Subscription', 'Subscription');
jasmine.restoreLibrary('../src/LiveQuery/QueryTools', 'queryHash');
jasmine.restoreLibrary('../src/LiveQuery/QueryTools', 'matchesQuery');
jasmine.restoreLibrary('tv4', 'validate');
jasmine.restoreLibrary('../src/LiveQuery/ParsePubSub', 'ParsePubSub');
jasmine.restoreLibrary('../src/LiveQuery/SessionTokenCache', 'SessionTokenCache');
});
// Helper functions to add mock client and subscription to a liveQueryServer
function addMockClient(parseLiveQueryServer, clientId) {
var Client = require('../src/LiveQuery/Client').Client;
var client = new Client(clientId, {});
parseLiveQueryServer.clients.set(clientId, client);
return client;
}
function addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query) {
// If parseWebSocket is null, we use the default one
if (!parseWebSocket) {
var 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' ]
};
}
var request = {
query: query,
requestId: requestId,
sessionToken: 'sessionToken'
};
parseLiveQueryServer._handleSubscribe(parseWebSocket, request);
// Make mock subscription
var subscription = parseLiveQueryServer.subscriptions.get(query.className).get(queryHashValue);
subscription.hasSubscribingClient = function() {
return false;
}
subscription.className = query.className;
subscription.hash = 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) {
var parseObject = new Parse.Object(testClassName);
parseObject._finishFetch({
key: 'value',
className: testClassName
});
var message = {
currentParseObject: parseObject
};
if (hasOriginalParseObject) {
var originalParseObject = new Parse.Object(testClassName);
originalParseObject._finishFetch({
key: 'originalValue',
className: testClassName
});
message.originalParseObject = originalParseObject;
}
return message;
}
});

65
spec/ParsePubSub.spec.js Normal file
View File

@@ -0,0 +1,65 @@
var ParsePubSub = require('../src/LiveQuery/ParsePubSub').ParsePubSub;
describe('ParsePubSub', function() {
beforeEach(function(done) {
// Mock RedisPubSub
var mockRedisPubSub = {
createPublisher: jasmine.createSpy('createPublisherRedis'),
createSubscriber: jasmine.createSpy('createSubscriberRedis')
};
jasmine.mockLibrary('../src/LiveQuery/RedisPubSub', 'RedisPubSub', mockRedisPubSub);
// Mock EventEmitterPubSub
var mockEventEmitterPubSub = {
createPublisher: jasmine.createSpy('createPublisherEventEmitter'),
createSubscriber: jasmine.createSpy('createSubscriberEventEmitter')
};
jasmine.mockLibrary('../src/LiveQuery/EventEmitterPubSub', 'EventEmitterPubSub', mockEventEmitterPubSub);
done();
});
it('can create redis publisher', function() {
var publisher = ParsePubSub.createPublisher({
redisURL: 'redisURL'
});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createPublisher).toHaveBeenCalledWith('redisURL');
expect(EventEmitterPubSub.createPublisher).not.toHaveBeenCalled();
});
it('can create event emitter publisher', function() {
var publisher = ParsePubSub.createPublisher({});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createPublisher).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createPublisher).toHaveBeenCalled();
});
it('can create redis subscriber', function() {
var subscriber = ParsePubSub.createSubscriber({
redisURL: 'redisURL'
});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createSubscriber).toHaveBeenCalledWith('redisURL');
expect(EventEmitterPubSub.createSubscriber).not.toHaveBeenCalled();
});
it('can create event emitter subscriber', function() {
var subscriptionInfos = ParsePubSub.createSubscriber({});
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
var EventEmitterPubSub = require('../src/LiveQuery/EventEmitterPubSub').EventEmitterPubSub;
expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled();
expect(EventEmitterPubSub.createSubscriber).toHaveBeenCalled();
});
afterEach(function(){
jasmine.restoreLibrary('../src/LiveQuery/RedisPubSub', 'RedisPubSub');
jasmine.restoreLibrary('../src/LiveQuery/EventEmitterPubSub', 'EventEmitterPubSub');
});
});

View File

@@ -0,0 +1,43 @@
var ParseWebSocket = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocket;
describe('ParseWebSocket', function() {
it('can be initialized', function() {
var ws = {};
var parseWebSocket = new ParseWebSocket(ws);
expect(parseWebSocket.ws).toBe(ws);
});
it('can handle events defined in typeMap', function() {
var ws = {
on: jasmine.createSpy('on')
};
var callback = {};
var parseWebSocket = new ParseWebSocket(ws);
parseWebSocket.on('disconnect', callback);
expect(parseWebSocket.ws.on).toHaveBeenCalledWith('close', callback);
});
it('can handle events which are not defined in typeMap', function() {
var ws = {
on: jasmine.createSpy('on')
};
var callback = {};
var parseWebSocket = new ParseWebSocket(ws);
parseWebSocket.on('open', callback);
expect(parseWebSocket.ws.on).toHaveBeenCalledWith('open', callback);
});
it('can send a message', function() {
var ws = {
send: jasmine.createSpy('send')
};
var parseWebSocket = new ParseWebSocket(ws);
parseWebSocket.send('message')
expect(parseWebSocket.ws.send).toHaveBeenCalledWith('message');
});
});

View File

@@ -0,0 +1,37 @@
var ParseWebSocketServer = require('../src/LiveQuery/ParseWebSocketServer').ParseWebSocketServer;
describe('ParseWebSocketServer', function() {
beforeEach(function(done) {
// Mock ws server
var EventEmitter = require('events');
var mockServer = function() {
return new EventEmitter();
};
jasmine.mockLibrary('ws', 'Server', mockServer);
done();
});
it('can handle connect event when ws is open', function(done) {
var onConnectCallback = jasmine.createSpy('onConnectCallback');
var parseWebSocketServer = new ParseWebSocketServer({}, onConnectCallback, 5).server;
var ws = {
readyState: 0,
OPEN: 0,
ping: jasmine.createSpy('ping')
};
parseWebSocketServer.emit('connection', ws);
// Make sure callback is called
expect(onConnectCallback).toHaveBeenCalled();
// Make sure we ping to the client
setTimeout(function() {
expect(ws.ping).toHaveBeenCalled();
done();
}, 10)
});
afterEach(function(){
jasmine.restoreLibrary('ws', 'Server');
});
});

384
spec/QueryTools.spec.js Normal file
View File

@@ -0,0 +1,384 @@
var Parse = require('parse/node');
var Id = require('../src/LiveQuery/Id');
var QueryTools = require('../src/LiveQuery/QueryTools');
var queryHash = QueryTools.queryHash;
var matchesQuery = QueryTools.matchesQuery;
var Item = Parse.Object.extend('Item');
describe('queryHash', function() {
it('should always hash a query to the same string', function() {
var q = new Parse.Query(Item);
q.equalTo('field', 'value');
q.exists('name');
q.ascending('createdAt');
q.limit(10);
var firstHash = queryHash(q);
var secondHash = queryHash(q);
expect(firstHash).toBe(secondHash);
});
it('should return equivalent hashes for equivalent queries', function() {
var q1 = new Parse.Query(Item);
q1.equalTo('field', 'value');
q1.exists('name');
q1.lessThan('age', 30);
q1.greaterThan('age', 3);
q1.ascending('createdAt');
q1.include(['name', 'age']);
q1.limit(10);
var q2 = new Parse.Query(Item);
q2.limit(10);
q2.greaterThan('age', 3);
q2.lessThan('age', 30);
q2.include(['name', 'age']);
q2.ascending('createdAt');
q2.exists('name');
q2.equalTo('field', 'value');
var firstHash = queryHash(q1);
var secondHash = queryHash(q2);
expect(firstHash).toBe(secondHash);
q1.containedIn('fruit', ['apple', 'banana', 'cherry']);
firstHash = queryHash(q1);
expect(firstHash).not.toBe(secondHash);
q2.containedIn('fruit', ['banana', 'cherry', 'apple']);
secondHash = queryHash(q2);
expect(secondHash).toBe(firstHash);
q1.containedIn('fruit', ['coconut']);
firstHash = queryHash(q1);
expect(firstHash).not.toBe(secondHash);
q1 = new Parse.Query(Item);
q1.equalTo('field', 'value');
q1.lessThan('age', 30);
q1.exists('name');
q2 = new Parse.Query(Item);
q2.equalTo('name', 'person');
q2.equalTo('field', 'other');
firstHash = queryHash(Parse.Query.or(q1, q2));
secondHash = queryHash(Parse.Query.or(q2, q1));
expect(firstHash).toBe(secondHash);
});
it('should not let fields of different types appear similar', function() {
var q1 = new Parse.Query(Item);
q1.lessThan('age', 30);
var q2 = new Parse.Query(Item);
q2.equalTo('age', '{$lt:30}');
expect(queryHash(q1)).not.toBe(queryHash(q2));
q1 = new Parse.Query(Item);
q1.equalTo('age', 15);
q2.equalTo('age', '15');
expect(queryHash(q1)).not.toBe(queryHash(q2));
});
});
describe('matchesQuery', function() {
it('matches blanket queries', function() {
var obj = {
id: new Id('Klass', 'O1'),
value: 12
};
var q = new Parse.Query('Klass');
expect(matchesQuery(obj, q)).toBe(true);
obj.id = new Id('Other', 'O1');
expect(matchesQuery(obj, q)).toBe(false);
});
it('matches existence queries', function() {
var obj = {
id: new Id('Item', 'O1'),
count: 15
};
var q = new Parse.Query('Item');
q.exists('count');
expect(matchesQuery(obj, q)).toBe(true);
q.exists('name');
expect(matchesQuery(obj, q)).toBe(false);
});
it('matches on equality queries', function() {
var day = new Date();
var location = new Parse.GeoPoint({
latitude: 37.484815,
longitude: -122.148377
});
var obj = {
id: new Id('Person', 'O1'),
score: 12,
name: 'Bill',
birthday: day,
lastLocation: location
};
var q = new Parse.Query('Person');
q.equalTo('score', 12);
expect(matchesQuery(obj, q)).toBe(true);
q = new Parse.Query('Person');
q.equalTo('name', 'Bill');
expect(matchesQuery(obj, q)).toBe(true);
q.equalTo('name', 'Jeff');
expect(matchesQuery(obj, q)).toBe(false);
q = new Parse.Query('Person');
q.containedIn('name', ['Adam', 'Ben', 'Charles']);
expect(matchesQuery(obj, q)).toBe(false);
q.containedIn('name', ['Adam', 'Bill', 'Charles']);
expect(matchesQuery(obj, q)).toBe(true);
q = new Parse.Query('Person');
q.notContainedIn('name', ['Adam', 'Bill', 'Charles']);
expect(matchesQuery(obj, q)).toBe(false);
q.notContainedIn('name', ['Adam', 'Ben', 'Charles']);
expect(matchesQuery(obj, q)).toBe(true);
q = new Parse.Query('Person');
q.equalTo('birthday', day);
expect(matchesQuery(obj, q)).toBe(true);
q.equalTo('birthday', new Date());
expect(matchesQuery(obj, q)).toBe(false);
q = new Parse.Query('Person');
q.equalTo('lastLocation', new Parse.GeoPoint({
latitude: 37.484815,
longitude: -122.148377
}));
expect(matchesQuery(obj, q)).toBe(true);
q.equalTo('lastLocation', new Parse.GeoPoint({
latitude: 37.4848,
longitude: -122.1483
}));
expect(matchesQuery(obj, q)).toBe(false);
q.equalTo('lastLocation', new Parse.GeoPoint({
latitude: 37.484815,
longitude: -122.148377
}));
q.equalTo('score', 12);
q.equalTo('name', 'Bill');
q.equalTo('birthday', day);
expect(matchesQuery(obj, q)).toBe(true);
q.equalTo('name', 'bill');
expect(matchesQuery(obj, q)).toBe(false);
var img = {
id: new Id('Image', 'I1'),
tags: ['nofilter', 'latergram', 'tbt']
};
q = new Parse.Query('Image');
q.equalTo('tags', 'selfie');
expect(matchesQuery(img, q)).toBe(false);
q.equalTo('tags', 'tbt');
expect(matchesQuery(img, q)).toBe(true);
var q2 = new Parse.Query('Image');
q2.containsAll('tags', ['latergram', 'nofilter']);
expect(matchesQuery(img, q2)).toBe(true);
q2.containsAll('tags', ['latergram', 'selfie']);
expect(matchesQuery(img, q2)).toBe(false);
var u = new Parse.User();
u.id = 'U2';
q = new Parse.Query('Image');
q.equalTo('owner', u);
img = {
className: 'Image',
objectId: 'I1',
owner: {
className: '_User',
objectId: 'U2'
}
};
expect(matchesQuery(img, q)).toBe(true);
img.owner.objectId = 'U3';
expect(matchesQuery(img, q)).toBe(false);
});
it('matches on inequalities', function() {
var player = {
id: new Id('Person', 'O1'),
score: 12,
name: 'Bill',
birthday: new Date(1980, 2, 4),
};
var q = new Parse.Query('Person');
q.lessThan('score', 15);
expect(matchesQuery(player, q)).toBe(true);
q.lessThan('score', 10);
expect(matchesQuery(player, q)).toBe(false);
q = new Parse.Query('Person');
q.lessThanOrEqualTo('score', 15);
expect(matchesQuery(player, q)).toBe(true);
q.lessThanOrEqualTo('score', 12);
expect(matchesQuery(player, q)).toBe(true);
q.lessThanOrEqualTo('score', 10);
expect(matchesQuery(player, q)).toBe(false);
q = new Parse.Query('Person');
q.greaterThan('score', 15);
expect(matchesQuery(player, q)).toBe(false);
q.greaterThan('score', 10);
expect(matchesQuery(player, q)).toBe(true);
q = new Parse.Query('Person');
q.greaterThanOrEqualTo('score', 15);
expect(matchesQuery(player, q)).toBe(false);
q.greaterThanOrEqualTo('score', 12);
expect(matchesQuery(player, q)).toBe(true);
q.greaterThanOrEqualTo('score', 10);
expect(matchesQuery(player, q)).toBe(true);
q = new Parse.Query('Person');
q.notEqualTo('score', 12);
expect(matchesQuery(player, q)).toBe(false);
q.notEqualTo('score', 40);
expect(matchesQuery(player, q)).toBe(true);
});
it('matches an $or query', function() {
var player = {
id: new Id('Player', 'P1'),
name: 'Player 1',
score: 12
};
var q = new Parse.Query('Player');
q.equalTo('name', 'Player 1');
var q2 = new Parse.Query('Player');
q2.equalTo('name', 'Player 2');
var orQuery = Parse.Query.or(q, q2);
expect(matchesQuery(player, q)).toBe(true);
expect(matchesQuery(player, q2)).toBe(false);
expect(matchesQuery(player, orQuery)).toBe(true);
});
it('matches $regex queries', function() {
var player = {
id: new Id('Player', 'P1'),
name: 'Player 1',
score: 12
};
var q = new Parse.Query('Player');
q.startsWith('name', 'Play');
expect(matchesQuery(player, q)).toBe(true);
q.startsWith('name', 'Ploy');
expect(matchesQuery(player, q)).toBe(false);
q = new Parse.Query('Player');
q.endsWith('name', ' 1');
expect(matchesQuery(player, q)).toBe(true);
q.endsWith('name', ' 2');
expect(matchesQuery(player, q)).toBe(false);
// Check that special characters are escaped
player.name = 'Android-7';
q = new Parse.Query('Player');
q.contains('name', 'd-7');
expect(matchesQuery(player, q)).toBe(true);
q = new Parse.Query('Player');
q.matches('name', /A.d/);
expect(matchesQuery(player, q)).toBe(true);
q.matches('name', /A[^n]d/);
expect(matchesQuery(player, q)).toBe(false);
// Check that the string \\E is returned to normal
player.name = 'Slash \\E';
q = new Parse.Query('Player');
q.endsWith('name', 'h \\E');
expect(matchesQuery(player, q)).toBe(true);
q.endsWith('name', 'h \\Ee');
expect(matchesQuery(player, q)).toBe(false);
player.name = 'Slash \\Q and more';
q = new Parse.Query('Player');
q.contains('name', 'h \\Q and');
expect(matchesQuery(player, q)).toBe(true);
q.contains('name', 'h \\Q or');
expect(matchesQuery(player, q)).toBe(false);
});
it('matches $nearSphere queries', function() {
var q = new Parse.Query('Checkin');
q.near('location', new Parse.GeoPoint(20, 20));
// With no max distance, any GeoPoint is 'near'
var pt = {
id: new Id('Checkin', 'C1'),
location: new Parse.GeoPoint(40, 40)
};
expect(matchesQuery(pt, q)).toBe(true);
q = new Parse.Query('Checkin');
pt.location = new Parse.GeoPoint(40, 40);
q.withinRadians('location', new Parse.GeoPoint(30, 30), 0.3);
expect(matchesQuery(pt, q)).toBe(true);
q.withinRadians('location', new Parse.GeoPoint(30, 30), 0.2);
expect(matchesQuery(pt, q)).toBe(false);
});
it('matches $within queries', function() {
var caltrainStation = {
id: new Id('Checkin', 'C1'),
location: new Parse.GeoPoint(37.776346, -122.394218),
name: 'Caltrain'
};
var santaClara = {
id: new Id('Checkin', 'C2'),
location: new Parse.GeoPoint(37.325635, -121.945753),
name: 'Santa Clara'
};
var q = new Parse.Query('Checkin').withinGeoBox(
'location',
new Parse.GeoPoint(37.708813, -122.526398),
new Parse.GeoPoint(37.822802, -122.373962)
);
expect(matchesQuery(caltrainStation, q)).toBe(true);
expect(matchesQuery(santaClara, q)).toBe(false);
// Invalid rectangles
q = new Parse.Query('Checkin').withinGeoBox(
'location',
new Parse.GeoPoint(37.822802, -122.373962),
new Parse.GeoPoint(37.708813, -122.526398)
);
expect(matchesQuery(caltrainStation, q)).toBe(false);
expect(matchesQuery(santaClara, q)).toBe(false);
q = new Parse.Query('Checkin').withinGeoBox(
'location',
new Parse.GeoPoint(37.708813, -122.373962),
new Parse.GeoPoint(37.822802, -122.526398)
);
expect(matchesQuery(caltrainStation, q)).toBe(false);
expect(matchesQuery(santaClara, q)).toBe(false);
});
});

29
spec/RedisPubSub.spec.js Normal file
View File

@@ -0,0 +1,29 @@
var RedisPubSub = require('../src/LiveQuery/RedisPubSub').RedisPubSub;
describe('RedisPubSub', function() {
beforeEach(function(done) {
// Mock redis
var createClient = jasmine.createSpy('createClient');
jasmine.mockLibrary('redis', 'createClient', createClient);
done();
});
it('can create publisher', function() {
var publisher = RedisPubSub.createPublisher('redisAddress');
var redis = require('redis');
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { no_ready_check: true });
});
it('can create subscriber', function() {
var subscriber = RedisPubSub.createSubscriber('redisAddress');
var redis = require('redis');
expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { no_ready_check: true });
});
afterEach(function() {
jasmine.restoreLibrary('redis', 'createClient');
});
});

View File

@@ -0,0 +1,52 @@
var SessionTokenCache = require('../src/LiveQuery/SessionTokenCache').SessionTokenCache;
describe('SessionTokenCache', function() {
beforeEach(function(done) {
var Parse = require('parse/node');
// Mock parse
var mockUser = {
become: jasmine.createSpy('become').and.returnValue(Parse.Promise.as({
id: 'userId'
}))
}
jasmine.mockLibrary('parse/node', 'User', mockUser);
done();
});
it('can get undefined userId', function(done) {
var sessionTokenCache = new SessionTokenCache();
sessionTokenCache.getUserId(undefined).then((userIdFromCache) => {
}, (error) => {
expect(error).not.toBeNull();
done();
});
});
it('can get existing userId', function(done) {
var sessionTokenCache = new SessionTokenCache();
var sessionToken = 'sessionToken';
var userId = 'userId'
sessionTokenCache.cache.set(sessionToken, userId);
sessionTokenCache.getUserId(sessionToken).then((userIdFromCache) => {
expect(userIdFromCache).toBe(userId);
done();
});
});
it('can get new userId', function(done) {
var sessionTokenCache = new SessionTokenCache();
sessionTokenCache.getUserId('sessionToken').then((userIdFromCache) => {
expect(userIdFromCache).toBe('userId');
expect(sessionTokenCache.cache.length).toBe(1);
done();
});
});
afterEach(function() {
jasmine.restoreLibrary('parse/node', 'User');
});
});

123
spec/Subscription.spec.js Normal file
View File

@@ -0,0 +1,123 @@
var Subscription = require('../src/LiveQuery/Subscription').Subscription;
describe('Subscription', function() {
beforeEach(function() {
var mockError = jasmine.createSpy('error');
jasmine.mockLibrary('../src/LiveQuery/PLog', 'error', mockError);
});
it('can be initialized', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
expect(subscription.className).toBe('className');
expect(subscription.query).toEqual({ key : 'value' });
expect(subscription.hash).toBe('hash');
expect(subscription.clientRequestIds.size).toBe(0);
});
it('can check it has subscribing clients', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
expect(subscription.hasSubscribingClient()).toBe(false);
});
it('can check it does not have subscribing clients', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
expect(subscription.hasSubscribingClient()).toBe(true);
});
it('can add one request for one client', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
expect(subscription.clientRequestIds.size).toBe(1);
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
});
it('can add requests for one client', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
subscription.addClientSubscription(1, 2);
expect(subscription.clientRequestIds.size).toBe(1);
expect(subscription.clientRequestIds.get(1)).toEqual([1, 2]);
});
it('can add requests for clients', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
subscription.addClientSubscription(1, 2);
subscription.addClientSubscription(2, 2);
subscription.addClientSubscription(2, 3);
expect(subscription.clientRequestIds.size).toBe(2);
expect(subscription.clientRequestIds.get(1)).toEqual([1, 2]);
expect(subscription.clientRequestIds.get(2)).toEqual([2, 3]);
});
it('can delete requests for nonexistent client', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.deleteClientSubscription(1, 1);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).toHaveBeenCalled();
});
it('can delete nonexistent request for one client', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
subscription.deleteClientSubscription(1, 2);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).toHaveBeenCalled();
expect(subscription.clientRequestIds.size).toBe(1);
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
});
it('can delete some requests for one client', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
subscription.addClientSubscription(1, 2);
subscription.deleteClientSubscription(1, 2);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).not.toHaveBeenCalled();
expect(subscription.clientRequestIds.size).toBe(1);
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
});
it('can delete all requests for one client', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
subscription.addClientSubscription(1, 2);
subscription.deleteClientSubscription(1, 1);
subscription.deleteClientSubscription(1, 2);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).not.toHaveBeenCalled();
expect(subscription.clientRequestIds.size).toBe(0);
});
it('can delete requests for multiple clients', function() {
var subscription = new Subscription('className', { key : 'value' }, 'hash');
subscription.addClientSubscription(1, 1);
subscription.addClientSubscription(1, 2);
subscription.addClientSubscription(2, 1);
subscription.addClientSubscription(2, 2);
subscription.deleteClientSubscription(1, 2);
subscription.deleteClientSubscription(2, 1);
subscription.deleteClientSubscription(2, 2);
var PLog =require('../src/LiveQuery/PLog');
expect(PLog.error).not.toHaveBeenCalled();
expect(subscription.clientRequestIds.size).toBe(1);
expect(subscription.clientRequestIds.get(1)).toEqual([1]);
});
afterEach(function(){
jasmine.restoreLibrary('../src/LiveQuery/PLog', 'error');
});
});

View File

@@ -252,3 +252,22 @@ global.jequal = jequal;
global.range = range;
global.setServerConfiguration = setServerConfiguration;
global.defaultConfiguration = defaultConfiguration;
// LiveQuery test setting
require('../src/LiveQuery/PLog').logLevel = 'NONE';
var libraryCache = {};
jasmine.mockLibrary = function(library, name, mock) {
var original = require(library)[name];
if (!libraryCache[library]) {
libraryCache[library] = {};
}
require(library)[name] = mock;
libraryCache[library][name] = original;
}
jasmine.restoreLibrary = function(library, name) {
if (!libraryCache[library] || !libraryCache[library][name]) {
throw 'Can not find library ' + library + ' ' + name;
}
require(library)[name] = libraryCache[library][name];
}