Files
kami-parse-server/spec/ParseServerRESTController.spec.js
2024-07-21 12:11:03 +02:00

680 lines
22 KiB
JavaScript

const ParseServerRESTController = require('../lib/ParseServerRESTController')
.ParseServerRESTController;
const ParseServer = require('../lib/ParseServer').default;
const Parse = require('parse/node').Parse;
let RESTController;
describe('ParseServerRESTController', () => {
let createSpy;
beforeEach(() => {
RESTController = ParseServerRESTController(
Parse.applicationId,
ParseServer.promiseRouter({ appId: Parse.applicationId })
);
createSpy = spyOn(databaseAdapter, 'createObject').and.callThrough();
});
it('should handle a get request', async () => {
const res = await RESTController.request('GET', '/classes/MyObject');
expect(res.results.length).toBe(0);
});
it('should handle a get request with full serverURL mount path', async () => {
const res = await RESTController.request('GET', '/1/classes/MyObject');
expect(res.results.length).toBe(0);
});
it('should handle a POST batch without transaction', async () => {
const res = await RESTController.request('POST', 'batch', {
requests: [
{
method: 'GET',
path: '/classes/MyObject',
},
{
method: 'POST',
path: '/classes/MyObject',
body: { key: 'value' },
},
{
method: 'GET',
path: '/classes/MyObject',
},
],
});
expect(res.length).toBe(3);
});
it('should handle a POST batch with transaction=false', async () => {
const res = await RESTController.request('POST', 'batch', {
requests: [
{
method: 'GET',
path: '/classes/MyObject',
},
{
method: 'POST',
path: '/classes/MyObject',
body: { key: 'value' },
},
{
method: 'GET',
path: '/classes/MyObject',
},
],
transaction: false,
});
expect(res.length).toBe(3);
});
it('should handle response status', async () => {
const router = ParseServer.promiseRouter({ appId: Parse.applicationId });
spyOn(router, 'tryRouteRequest').and.callThrough();
RESTController = ParseServerRESTController(Parse.applicationId, router);
const resp = await RESTController.request('POST', '/classes/MyObject');
const { status, response, location } = await router.tryRouteRequest.calls.all()[0].returnValue;
expect(status).toBe(201);
expect(response).toEqual(resp);
expect(location).toBe(`http://localhost:8378/1/classes/MyObject/${resp.objectId}`);
});
it('should handle response status in batch', async () => {
const router = ParseServer.promiseRouter({ appId: Parse.applicationId });
spyOn(router, 'tryRouteRequest').and.callThrough();
RESTController = ParseServerRESTController(Parse.applicationId, router);
const resp = await RESTController.request(
'POST',
'batch',
{
requests: [
{
method: 'POST',
path: '/classes/MyObject',
},
{
method: 'POST',
path: '/classes/MyObject',
},
],
},
{
returnStatus: true,
}
);
expect(resp.length).toBe(2);
expect(resp[0]._status).toBe(201);
expect(resp[1]._status).toBe(201);
expect(resp[0].success).toBeDefined();
expect(resp[1].success).toBeDefined();
expect(router.tryRouteRequest.calls.all().length).toBe(2);
});
it('properly handle existed', async done => {
const restController = Parse.CoreManager.getRESTController();
Parse.CoreManager.setRESTController(RESTController);
Parse.Cloud.define('handleStatus', async () => {
const obj = new Parse.Object('TestObject');
expect(obj.existed()).toBe(false);
await obj.save();
expect(obj.existed()).toBe(false);
const query = new Parse.Query('TestObject');
const result = await query.get(obj.id);
expect(result.existed()).toBe(true);
Parse.CoreManager.setRESTController(restController);
done();
});
await Parse.Cloud.run('handleStatus');
});
if (
process.env.MONGODB_TOPOLOGY === 'replicaset' ||
process.env.PARSE_SERVER_TEST_DB === 'postgres'
) {
describe('transactions', () => {
it('should handle a batch request with transaction = true', async () => {
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
await myObject.save();
await myObject.destroy();
createSpy.calls.reset();
const response = await RESTController.request('POST', 'batch', {
requests: [
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value2' },
},
],
transaction: true,
});
expect(response.length).toEqual(2);
expect(response[0].success.objectId).toBeDefined();
expect(response[0].success.createdAt).toBeDefined();
expect(response[1].success.objectId).toBeDefined();
expect(response[1].success.createdAt).toBeDefined();
const query = new Parse.Query('MyObject');
const results = await query.find();
expect(createSpy.calls.count()).toBe(2);
for (let i = 0; i + 1 < createSpy.calls.length; i = i + 2) {
expect(createSpy.calls.argsFor(i)[3]).toBe(
createSpy.calls.argsFor(i + 1)[3]
);
}
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
});
it('should not save anything when one operation fails in a transaction', async () => {
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
await myObject.save({ key: 'stringField' });
await myObject.destroy();
createSpy.calls.reset();
try {
// Saving a number to a string field should fail
await RESTController.request('POST', 'batch', {
requests: [
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 10 },
},
],
transaction: true,
});
fail();
} catch (error) {
expect(error).toBeDefined();
const query = new Parse.Query('MyObject');
const results = await query.find();
expect(results.length).toBe(0);
}
});
it('should generate separate session for each call', async () => {
await reconfigureServer();
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
await myObject.save({ key: 'stringField' });
await myObject.destroy();
const myObject2 = new Parse.Object('MyObject2'); // This is important because transaction only works on pre-existing collections
await myObject2.save({ key: 'stringField' });
await myObject2.destroy();
createSpy.calls.reset();
let myObjectCalls = 0;
Parse.Cloud.beforeSave('MyObject', async () => {
myObjectCalls++;
if (myObjectCalls === 2) {
try {
// Saving a number to a string field should fail
await RESTController.request('POST', 'batch', {
requests: [
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 10 },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject2',
body: { key: 10 },
},
],
transaction: true,
});
fail('should fail');
} catch (e) {
expect(e).toBeDefined();
}
}
});
const response = await RESTController.request('POST', 'batch', {
requests: [
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value2' },
},
],
transaction: true,
});
expect(response.length).toEqual(2);
expect(response[0].success.objectId).toBeDefined();
expect(response[0].success.createdAt).toBeDefined();
expect(response[1].success.objectId).toBeDefined();
expect(response[1].success.createdAt).toBeDefined();
await RESTController.request('POST', 'batch', {
requests: [
{
method: 'POST',
path: '/1/classes/MyObject3',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject3',
body: { key: 'value2' },
},
],
});
const query = new Parse.Query('MyObject');
const results = await query.find();
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
const query2 = new Parse.Query('MyObject2');
const results2 = await query2.find();
expect(results2.length).toEqual(0);
const query3 = new Parse.Query('MyObject3');
const results3 = await query3.find();
expect(results3.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
expect(createSpy.calls.count() >= 13).toEqual(true);
let transactionalSession;
let transactionalSession2;
let myObjectDBCalls = 0;
let myObject2DBCalls = 0;
let myObject3DBCalls = 0;
for (let i = 0; i < createSpy.calls.count(); i++) {
const args = createSpy.calls.argsFor(i);
switch (args[0]) {
case 'MyObject':
myObjectDBCalls++;
if (!transactionalSession || (myObjectDBCalls - 1) % 2 === 0) {
transactionalSession = args[3];
} else {
expect(transactionalSession).toBe(args[3]);
}
if (transactionalSession2) {
expect(transactionalSession2).not.toBe(args[3]);
}
break;
case 'MyObject2':
myObject2DBCalls++;
if (!transactionalSession2 || (myObject2DBCalls - 1) % 9 === 0) {
transactionalSession2 = args[3];
} else {
expect(transactionalSession2).toBe(args[3]);
}
if (transactionalSession) {
expect(transactionalSession).not.toBe(args[3]);
}
break;
case 'MyObject3':
myObject3DBCalls++;
expect(args[3]).toEqual(null);
break;
}
}
expect(myObjectDBCalls % 2).toEqual(0);
expect(myObjectDBCalls > 0).toEqual(true);
expect(myObject2DBCalls % 9).toEqual(0);
expect(myObject2DBCalls > 0).toEqual(true);
expect(myObject3DBCalls % 2).toEqual(0);
expect(myObject3DBCalls > 0).toEqual(true);
});
});
}
it('should handle a POST request', async () => {
await RESTController.request('POST', '/classes/MyObject', { key: 'value' });
const res = await RESTController.request('GET', '/classes/MyObject');
expect(res.results.length).toBe(1);
expect(res.results[0].key).toEqual('value');
});
it('should handle a POST request with context', async () => {
Parse.Cloud.beforeSave('MyObject', req => {
expect(req.context.a).toEqual('a');
});
Parse.Cloud.afterSave('MyObject', req => {
expect(req.context.a).toEqual('a');
});
await RESTController.request(
'POST',
'/classes/MyObject',
{ key: 'value' },
{ context: { a: 'a' } }
);
});
it('ensures sessionTokens are properly handled', async () => {
const user = await Parse.User.signUp('user', 'pass');
const sessionToken = user.getSessionToken();
const res = await RESTController.request('GET', '/users/me', undefined, {
sessionToken,
});
// Result is in JSON format
expect(res.objectId).toEqual(user.id);
});
it('ensures masterKey is properly handled', async () => {
const user = await Parse.User.signUp('user', 'pass');
const userId = user.id;
await Parse.User.logOut();
const res = await RESTController.request('GET', '/classes/_User', undefined, {
useMasterKey: true,
});
expect(res.results.length).toBe(1);
expect(res.results[0].objectId).toEqual(userId);
});
it('ensures no user is created when passing an empty username', async () => {
try {
await RESTController.request('POST', '/classes/_User', {
username: '',
password: 'world',
});
fail('Success callback should not be called when passing an empty username.');
} catch (err) {
expect(err.code).toBe(Parse.Error.USERNAME_MISSING);
expect(err.message).toBe('bad or missing username');
}
});
it('ensures no user is created when passing an empty password', async () => {
try {
await RESTController.request('POST', '/classes/_User', {
username: 'hello',
password: '',
});
fail('Success callback should not be called when passing an empty password.');
} catch (err) {
expect(err.code).toBe(Parse.Error.PASSWORD_MISSING);
expect(err.message).toBe('password is required');
}
});
it('ensures no session token is created on creating users', async () => {
const user = await RESTController.request('POST', '/classes/_User', {
username: 'hello',
password: 'world',
});
expect(user.sessionToken).toBeUndefined();
const query = new Parse.Query('_Session');
const sessions = await query.find({ useMasterKey: true });
expect(sessions.length).toBe(0);
});
it('ensures a session token is created when passing installationId != cloud', async () => {
const user = await RESTController.request(
'POST',
'/classes/_User',
{ username: 'hello', password: 'world' },
{ installationId: 'my-installation' }
);
expect(user.sessionToken).not.toBeUndefined();
const query = new Parse.Query('_Session');
const sessions = await query.find({ useMasterKey: true });
expect(sessions.length).toBe(1);
expect(sessions[0].get('installationId')).toBe('my-installation');
});
it('ensures logIn is saved with installationId', async () => {
const installationId = 'installation123';
const user = await RESTController.request(
'POST',
'/classes/_User',
{ username: 'hello', password: 'world' },
{ installationId }
);
expect(user.sessionToken).not.toBeUndefined();
const query = new Parse.Query('_Session');
let sessions = await query.find({ useMasterKey: true });
expect(sessions.length).toBe(1);
expect(sessions[0].get('installationId')).toBe(installationId);
expect(sessions[0].get('sessionToken')).toBe(user.sessionToken);
const loggedUser = await RESTController.request(
'POST',
'/login',
{ username: 'hello', password: 'world' },
{ installationId }
);
expect(loggedUser.sessionToken).not.toBeUndefined();
sessions = await query.find({ useMasterKey: true });
// Should clean up old sessions with this installationId
expect(sessions.length).toBe(1);
expect(sessions[0].get('installationId')).toBe(installationId);
expect(sessions[0].get('sessionToken')).toBe(loggedUser.sessionToken);
});
it('returns a statusId when running jobs', async () => {
Parse.Cloud.job('CloudJob', () => {
return 'Cloud job completed';
});
const res = await RESTController.request(
'POST',
'/jobs/CloudJob',
{},
{ useMasterKey: true, returnStatus: true }
);
const jobStatusId = res._headers['X-Parse-Job-Status-Id'];
expect(jobStatusId).toBeDefined();
const result = await Parse.Cloud.getJobStatus(jobStatusId);
expect(result.id).toBe(jobStatusId);
});
it('returns a statusId when running push notifications', async () => {
const payload = {
data: { alert: 'We return status!' },
where: { deviceType: 'ios' },
};
const res = await RESTController.request('POST', '/push', payload, {
useMasterKey: true,
returnStatus: true,
});
const pushStatusId = res._headers['X-Parse-Push-Status-Id'];
expect(pushStatusId).toBeDefined();
const result = await Parse.Push.getPushStatus(pushStatusId);
expect(result.id).toBe(pushStatusId);
});
it('returns a statusId when running batch push notifications', async () => {
const payload = {
data: { alert: 'We return status!' },
where: { deviceType: 'ios' },
};
const res = await RESTController.request('POST', 'batch', {
requests: [{
method: 'POST',
path: '/push',
body: payload,
}],
}, {
useMasterKey: true,
returnStatus: true,
});
const pushStatusId = res[0]._headers['X-Parse-Push-Status-Id'];
expect(pushStatusId).toBeDefined();
const result = await Parse.Push.getPushStatus(pushStatusId);
expect(result.id).toBe(pushStatusId);
});
});