Files
kami-parse-server/spec/ParseServerRESTController.spec.js
Antonio Davi Macedo Coelho de Castro 8b97c1380b Batch transaction (#5849)
* Batch transaction boilerplate

* Refactoring transaction boilerplate

* Independent sessions test

* Transactions - partial

* Missing only one test

* All tests passing for mongo db

* Tests on Travis

* Transactions on postgres

* Fix travis to restart mongodb

* Remove mongodb service and keep only mongodb runner

* MongoDB service back

* Initialize replicaset

* Remove mongodb runner again

* Again only with mongodb-runner and removing cache

* Trying with pretest and posttest

* WiredTiger

* Pretest and posttest again

* Removing inexistent scripts

* wiredTiger

* One more attempt

* Trying another way to run mongodb-runner

* Fixing tests

* Include batch transaction on direct access

* Add tests to direct access
2019-07-31 02:41:07 -07:00

485 lines
14 KiB
JavaScript

const ParseServerRESTController = require('../lib/ParseServerRESTController')
.ParseServerRESTController;
const ParseServer = require('../lib/ParseServer').default;
const Parse = require('parse/node').Parse;
const TestUtils = require('../lib/TestUtils');
let RESTController;
describe('ParseServerRESTController', () => {
beforeEach(() => {
RESTController = ParseServerRESTController(
Parse.applicationId,
ParseServer.promiseRouter({ appId: Parse.applicationId })
);
});
it('should handle a get request', done => {
RESTController.request('GET', '/classes/MyObject').then(
res => {
expect(res.results.length).toBe(0);
done();
},
err => {
console.log(err);
jfail(err);
done();
}
);
});
it('should handle a get request with full serverURL mount path', done => {
RESTController.request('GET', '/1/classes/MyObject').then(
res => {
expect(res.results.length).toBe(0);
done();
},
err => {
jfail(err);
done();
}
);
});
it('should handle a POST batch without transaction', done => {
RESTController.request('POST', 'batch', {
requests: [
{
method: 'GET',
path: '/classes/MyObject',
},
{
method: 'POST',
path: '/classes/MyObject',
body: { key: 'value' },
},
{
method: 'GET',
path: '/classes/MyObject',
},
],
}).then(
res => {
expect(res.length).toBe(3);
done();
},
err => {
jfail(err);
done();
}
);
});
it('should handle a POST batch with transaction=false', done => {
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,
}).then(
res => {
expect(res.length).toBe(3);
done();
},
err => {
jfail(err);
done();
}
);
});
if (
(process.env.MONGODB_VERSION === '4.0.4' &&
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger') ||
process.env.PARSE_SERVER_TEST_DB === 'postgres'
) {
describe('transactions', () => {
beforeAll(async () => {
if (
process.env.MONGODB_VERSION === '4.0.4' &&
process.env.MONGODB_TOPOLOGY === 'replicaset' &&
process.env.MONGODB_STORAGE_ENGINE === 'wiredTiger'
) {
await reconfigureServer({
databaseAdapter: undefined,
databaseURI:
'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase?replicaSet=replicaset',
});
}
});
beforeEach(async () => {
await TestUtils.destroyAllDataPermanently(true);
});
it('should handle a batch request with transaction = true', done => {
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
myObject
.save()
.then(() => {
return myObject.destroy();
})
.then(() => {
spyOn(databaseAdapter, 'createObject').and.callThrough();
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,
}).then(response => {
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');
query.find().then(results => {
expect(databaseAdapter.createObject.calls.count()).toBe(2);
expect(databaseAdapter.createObject.calls.argsFor(0)[3]).toBe(
databaseAdapter.createObject.calls.argsFor(1)[3]
);
expect(results.map(result => result.get('key')).sort()).toEqual(
['value1', 'value2']
);
done();
});
});
});
});
it('should not save anything when one operation fails in a transaction', done => {
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
myObject
.save()
.then(() => {
return myObject.destroy();
})
.then(() => {
RESTController.request('POST', 'batch', {
requests: [
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 'value1' },
},
{
method: 'POST',
path: '/1/classes/MyObject',
body: { key: 10 },
},
],
transaction: true,
}).catch(error => {
expect(error.message).toBeDefined();
const query = new Parse.Query('MyObject');
query.find().then(results => {
expect(results.length).toBe(0);
done();
});
});
});
});
it('should generate separate session for each call', async () => {
const myObject = new Parse.Object('MyObject'); // This is important because transaction only works on pre-existing collections
await myObject.save();
await myObject.destroy();
const myObject2 = new Parse.Object('MyObject2'); // This is important because transaction only works on pre-existing collections
await myObject2.save();
await myObject2.destroy();
spyOn(databaseAdapter, 'createObject').and.callThrough();
let myObjectCalls = 0;
Parse.Cloud.beforeSave('MyObject', async () => {
myObjectCalls++;
if (myObjectCalls === 2) {
try {
await RESTController.request('POST', 'batch', {
requests: [
{
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(databaseAdapter.createObject.calls.count()).toBe(5);
let transactionalSession;
let transactionalSession2;
let myObjectDBCalls = 0;
let myObject2DBCalls = 0;
let myObject3DBCalls = 0;
for (let i = 0; i < 5; i++) {
const args = databaseAdapter.createObject.calls.argsFor(i);
switch (args[0]) {
case 'MyObject':
myObjectDBCalls++;
if (!transactionalSession) {
transactionalSession = args[3];
} else {
expect(transactionalSession).toBe(args[3]);
}
if (transactionalSession2) {
expect(transactionalSession2).not.toBe(args[3]);
}
break;
case 'MyObject2':
myObject2DBCalls++;
transactionalSession2 = args[3];
if (transactionalSession) {
expect(transactionalSession).not.toBe(args[3]);
}
break;
case 'MyObject3':
myObject3DBCalls++;
expect(args[3]).toEqual(null);
break;
}
}
expect(myObjectDBCalls).toEqual(2);
expect(myObject2DBCalls).toEqual(1);
expect(myObject3DBCalls).toEqual(2);
});
});
}
it('should handle a POST request', done => {
RESTController.request('POST', '/classes/MyObject', { key: 'value' })
.then(() => {
return RESTController.request('GET', '/classes/MyObject');
})
.then(res => {
expect(res.results.length).toBe(1);
expect(res.results[0].key).toEqual('value');
done();
})
.catch(err => {
console.log(err);
jfail(err);
done();
});
});
it('ensures sessionTokens are properly handled', done => {
let userId;
Parse.User.signUp('user', 'pass')
.then(user => {
userId = user.id;
const sessionToken = user.getSessionToken();
return RESTController.request('GET', '/users/me', undefined, {
sessionToken,
});
})
.then(res => {
// Result is in JSON format
expect(res.objectId).toEqual(userId);
done();
})
.catch(err => {
console.log(err);
jfail(err);
done();
});
});
it('ensures masterKey is properly handled', done => {
let userId;
Parse.User.signUp('user', 'pass')
.then(user => {
userId = user.id;
return Parse.User.logOut().then(() => {
return RESTController.request('GET', '/classes/_User', undefined, {
useMasterKey: true,
});
});
})
.then(
res => {
expect(res.results.length).toBe(1);
expect(res.results[0].objectId).toEqual(userId);
done();
},
err => {
jfail(err);
done();
}
);
});
it('ensures no user is created when passing an empty username', done => {
RESTController.request('POST', '/classes/_User', {
username: '',
password: 'world',
}).then(
() => {
jfail(
new Error(
'Success callback should not be called when passing an empty username.'
)
);
done();
},
err => {
expect(err.code).toBe(Parse.Error.USERNAME_MISSING);
expect(err.message).toBe('bad or missing username');
done();
}
);
});
it('ensures no user is created when passing an empty password', done => {
RESTController.request('POST', '/classes/_User', {
username: 'hello',
password: '',
}).then(
() => {
jfail(
new Error(
'Success callback should not be called when passing an empty password.'
)
);
done();
},
err => {
expect(err.code).toBe(Parse.Error.PASSWORD_MISSING);
expect(err.message).toBe('password is required');
done();
}
);
});
it('ensures no session token is created on creating users', done => {
RESTController.request('POST', '/classes/_User', {
username: 'hello',
password: 'world',
})
.then(user => {
expect(user.sessionToken).toBeUndefined();
const query = new Parse.Query('_Session');
return query.find({ useMasterKey: true });
})
.then(sessions => {
expect(sessions.length).toBe(0);
done();
}, done.fail);
});
it('ensures a session token is created when passing installationId != cloud', done => {
RESTController.request(
'POST',
'/classes/_User',
{ username: 'hello', password: 'world' },
{ installationId: 'my-installation' }
)
.then(user => {
expect(user.sessionToken).not.toBeUndefined();
const query = new Parse.Query('_Session');
return query.find({ useMasterKey: true });
})
.then(
sessions => {
expect(sessions.length).toBe(1);
expect(sessions[0].get('installationId')).toBe('my-installation');
done();
},
err => {
jfail(err);
done();
}
);
});
});