mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
913 lines
29 KiB
JavaScript
913 lines
29 KiB
JavaScript
const batch = require('../lib/batch');
|
|
const request = require('../lib/request');
|
|
|
|
const originalURL = '/parse/batch';
|
|
const serverURL = 'http://localhost:1234/parse';
|
|
const serverURL1 = 'http://localhost:1234/1';
|
|
const serverURLNaked = 'http://localhost:1234/';
|
|
const publicServerURL = 'http://domain.com/parse';
|
|
const publicServerURLNaked = 'http://domain.com/';
|
|
const publicServerURLLong = 'https://domain.com/something/really/long';
|
|
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'X-Parse-Application-Id': 'test',
|
|
'X-Parse-REST-API-Key': 'rest',
|
|
'X-Parse-Installation-Id': 'yolo',
|
|
};
|
|
|
|
describe('batch', () => {
|
|
let createSpy;
|
|
beforeEach(async () => {
|
|
createSpy = spyOn(databaseAdapter, 'createObject').and.callThrough();
|
|
});
|
|
|
|
it('should return the proper url', () => {
|
|
const internalURL = batch.makeBatchRoutingPathFunction(originalURL)('/parse/classes/Object');
|
|
expect(internalURL).toEqual('/classes/Object');
|
|
});
|
|
|
|
it('should return the proper url given a public url-only path', () => {
|
|
const originalURL = '/something/really/long/batch';
|
|
const internalURL = batch.makeBatchRoutingPathFunction(
|
|
originalURL,
|
|
serverURL,
|
|
publicServerURLLong
|
|
)('/parse/classes/Object');
|
|
expect(internalURL).toEqual('/classes/Object');
|
|
});
|
|
|
|
it('should return the proper url given a server url-only path', () => {
|
|
const originalURL = '/parse/batch';
|
|
const internalURL = batch.makeBatchRoutingPathFunction(
|
|
originalURL,
|
|
serverURL,
|
|
publicServerURLLong
|
|
)('/parse/classes/Object');
|
|
expect(internalURL).toEqual('/classes/Object');
|
|
});
|
|
|
|
it('should return the proper url same public/local endpoint', () => {
|
|
const originalURL = '/parse/batch';
|
|
const internalURL = batch.makeBatchRoutingPathFunction(
|
|
originalURL,
|
|
serverURL,
|
|
publicServerURL
|
|
)('/parse/classes/Object');
|
|
|
|
expect(internalURL).toEqual('/classes/Object');
|
|
});
|
|
|
|
it('should return the proper url with different public/local mount', () => {
|
|
const originalURL = '/parse/batch';
|
|
const internalURL = batch.makeBatchRoutingPathFunction(
|
|
originalURL,
|
|
serverURL1,
|
|
publicServerURL
|
|
)('/parse/classes/Object');
|
|
|
|
expect(internalURL).toEqual('/classes/Object');
|
|
});
|
|
|
|
it('should return the proper url with naked public', () => {
|
|
const originalURL = '/batch';
|
|
const internalURL = batch.makeBatchRoutingPathFunction(
|
|
originalURL,
|
|
serverURL,
|
|
publicServerURLNaked
|
|
)('/classes/Object');
|
|
|
|
expect(internalURL).toEqual('/classes/Object');
|
|
});
|
|
|
|
it('should return the proper url with naked local', () => {
|
|
const originalURL = '/parse/batch';
|
|
const internalURL = batch.makeBatchRoutingPathFunction(
|
|
originalURL,
|
|
serverURLNaked,
|
|
publicServerURL
|
|
)('/parse/classes/Object');
|
|
|
|
expect(internalURL).toEqual('/classes/Object');
|
|
});
|
|
|
|
it('should return the proper url with no url provided', () => {
|
|
const originalURL = '/parse/batch';
|
|
const internalURL = batch.makeBatchRoutingPathFunction(
|
|
originalURL,
|
|
undefined,
|
|
publicServerURL
|
|
)('/parse/classes/Object');
|
|
|
|
expect(internalURL).toEqual('/classes/Object');
|
|
});
|
|
|
|
it('should return the proper url with no public url provided', () => {
|
|
const originalURL = '/parse/batch';
|
|
const internalURL = batch.makeBatchRoutingPathFunction(
|
|
originalURL,
|
|
serverURLNaked,
|
|
undefined
|
|
)('/parse/classes/Object');
|
|
|
|
expect(internalURL).toEqual('/classes/Object');
|
|
});
|
|
|
|
it('should return the proper url with bad url provided', () => {
|
|
const originalURL = '/parse/batch';
|
|
const internalURL = batch.makeBatchRoutingPathFunction(
|
|
originalURL,
|
|
'badurl.com',
|
|
publicServerURL
|
|
)('/parse/classes/Object');
|
|
|
|
expect(internalURL).toEqual('/classes/Object');
|
|
});
|
|
|
|
it('should return the proper url with bad public url provided', () => {
|
|
const originalURL = '/parse/batch';
|
|
const internalURL = batch.makeBatchRoutingPathFunction(
|
|
originalURL,
|
|
serverURLNaked,
|
|
'badurl.com'
|
|
)('/parse/classes/Object');
|
|
|
|
expect(internalURL).toEqual('/classes/Object');
|
|
});
|
|
|
|
it('should handle a batch request without transaction', async () => {
|
|
const response = await request({
|
|
method: 'POST',
|
|
headers: headers,
|
|
url: 'http://localhost:8378/1/batch',
|
|
body: JSON.stringify({
|
|
requests: [
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value2' },
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
expect(response.data.length).toEqual(2);
|
|
expect(response.data[0].success.objectId).toBeDefined();
|
|
expect(response.data[0].success.createdAt).toBeDefined();
|
|
expect(response.data[1].success.objectId).toBeDefined();
|
|
expect(response.data[1].success.createdAt).toBeDefined();
|
|
const query = new Parse.Query('MyObject');
|
|
const results = await query.find();
|
|
expect(createSpy.calls.count()).toBe(2);
|
|
expect(createSpy.calls.argsFor(0)[3]).toEqual(null);
|
|
expect(createSpy.calls.argsFor(1)[3]).toEqual(null);
|
|
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
|
|
});
|
|
|
|
it('should handle a batch request with transaction = false', async () => {
|
|
const response = await request({
|
|
method: 'POST',
|
|
headers: headers,
|
|
url: 'http://localhost:8378/1/batch',
|
|
body: JSON.stringify({
|
|
requests: [
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value2' },
|
|
},
|
|
],
|
|
transaction: false,
|
|
}),
|
|
});
|
|
expect(response.data.length).toEqual(2);
|
|
expect(response.data[0].success.objectId).toBeDefined();
|
|
expect(response.data[0].success.createdAt).toBeDefined();
|
|
expect(response.data[1].success.objectId).toBeDefined();
|
|
expect(response.data[1].success.createdAt).toBeDefined();
|
|
|
|
const query = new Parse.Query('MyObject');
|
|
const results = await query.find();
|
|
expect(createSpy.calls.count()).toBe(2);
|
|
expect(createSpy.calls.argsFor(0)[3]).toEqual(null);
|
|
expect(createSpy.calls.argsFor(1)[3]).toEqual(null);
|
|
expect(results.map(result => result.get('key')).sort()).toEqual(['value1', 'value2']);
|
|
});
|
|
|
|
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 request({
|
|
method: 'POST',
|
|
headers: headers,
|
|
url: 'http://localhost:8378/1/batch',
|
|
body: JSON.stringify({
|
|
requests: [
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value2' },
|
|
},
|
|
],
|
|
transaction: true,
|
|
}),
|
|
});
|
|
expect(response.data.length).toEqual(2);
|
|
expect(response.data[0].success.objectId).toBeDefined();
|
|
expect(response.data[0].success.createdAt).toBeDefined();
|
|
expect(response.data[1].success.objectId).toBeDefined();
|
|
expect(response.data[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 request({
|
|
method: 'POST',
|
|
headers: headers,
|
|
url: 'http://localhost:8378/1/batch',
|
|
body: JSON.stringify({
|
|
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 () => {
|
|
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 request({
|
|
method: 'POST',
|
|
headers: headers,
|
|
url: 'http://localhost:8378/1/batch',
|
|
body: JSON.stringify({
|
|
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 request({
|
|
method: 'POST',
|
|
headers: headers,
|
|
url: 'http://localhost:8378/1/batch',
|
|
body: JSON.stringify({
|
|
requests: [
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value1' },
|
|
},
|
|
{
|
|
method: 'POST',
|
|
path: '/1/classes/MyObject',
|
|
body: { key: 'value2' },
|
|
},
|
|
],
|
|
transaction: true,
|
|
}),
|
|
});
|
|
|
|
expect(response.data.length).toEqual(2);
|
|
expect(response.data[0].success.objectId).toBeDefined();
|
|
expect(response.data[0].success.createdAt).toBeDefined();
|
|
expect(response.data[1].success.objectId).toBeDefined();
|
|
expect(response.data[1].success.createdAt).toBeDefined();
|
|
|
|
await request({
|
|
method: 'POST',
|
|
headers: headers,
|
|
url: 'http://localhost:8378/1/batch',
|
|
body: JSON.stringify({
|
|
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);
|
|
});
|
|
});
|
|
}
|
|
|
|
describe('batch request size limit', () => {
|
|
it('should reject batch request when sub-requests exceed batchRequestLimit', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { batchRequestLimit: 2 },
|
|
});
|
|
await expectAsync(
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers,
|
|
body: JSON.stringify({
|
|
requests: [
|
|
{ method: 'GET', path: '/1/classes/TestClass' },
|
|
{ method: 'GET', path: '/1/classes/TestClass' },
|
|
{ method: 'GET', path: '/1/classes/TestClass' },
|
|
],
|
|
}),
|
|
})
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
status: 400,
|
|
data: jasmine.objectContaining({
|
|
error: jasmine.stringContaining('3'),
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should allow batch request when sub-requests are within batchRequestLimit', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { batchRequestLimit: 5 },
|
|
});
|
|
const result = await request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers,
|
|
body: JSON.stringify({
|
|
requests: [
|
|
{ method: 'POST', path: '/1/classes/TestClass', body: { key: 'v1' } },
|
|
{ method: 'POST', path: '/1/classes/TestClass', body: { key: 'v2' } },
|
|
],
|
|
}),
|
|
});
|
|
expect(result.data.length).toEqual(2);
|
|
expect(result.data[0].success.objectId).toBeDefined();
|
|
expect(result.data[1].success.objectId).toBeDefined();
|
|
});
|
|
|
|
it('should allow batch request at exactly batchRequestLimit', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { batchRequestLimit: 2 },
|
|
});
|
|
const result = await request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers,
|
|
body: JSON.stringify({
|
|
requests: [
|
|
{ method: 'POST', path: '/1/classes/TestClass', body: { key: 'v1' } },
|
|
{ method: 'POST', path: '/1/classes/TestClass', body: { key: 'v2' } },
|
|
],
|
|
}),
|
|
});
|
|
expect(result.data.length).toEqual(2);
|
|
});
|
|
|
|
it('should not limit batch request when batchRequestLimit is -1 (disabled)', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { batchRequestLimit: -1 },
|
|
});
|
|
const requests = Array.from({ length: 20 }, (_, i) => ({
|
|
method: 'POST',
|
|
path: '/1/classes/TestClass',
|
|
body: { key: `v${i}` },
|
|
}));
|
|
const result = await request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers,
|
|
body: JSON.stringify({ requests }),
|
|
});
|
|
expect(result.data.length).toEqual(20);
|
|
});
|
|
|
|
it('should not limit batch request by default (no requestComplexity configured)', async () => {
|
|
const requests = Array.from({ length: 20 }, (_, i) => ({
|
|
method: 'POST',
|
|
path: '/1/classes/TestClass',
|
|
body: { key: `v${i}` },
|
|
}));
|
|
const result = await request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers,
|
|
body: JSON.stringify({ requests }),
|
|
});
|
|
expect(result.data.length).toEqual(20);
|
|
});
|
|
|
|
it('should bypass batchRequestLimit for master key requests', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { batchRequestLimit: 2 },
|
|
});
|
|
const result = await request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers: {
|
|
...headers,
|
|
'X-Parse-Master-Key': 'test',
|
|
},
|
|
body: JSON.stringify({
|
|
requests: [
|
|
{ method: 'GET', path: '/1/classes/TestClass' },
|
|
{ method: 'GET', path: '/1/classes/TestClass' },
|
|
{ method: 'GET', path: '/1/classes/TestClass' },
|
|
],
|
|
}),
|
|
});
|
|
expect(result.data.length).toEqual(3);
|
|
});
|
|
|
|
it('should bypass batchRequestLimit for maintenance key requests', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { batchRequestLimit: 2 },
|
|
});
|
|
const result = await request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers: {
|
|
...headers,
|
|
'X-Parse-Maintenance-Key': 'testing',
|
|
},
|
|
body: JSON.stringify({
|
|
requests: [
|
|
{ method: 'GET', path: '/1/classes/TestClass' },
|
|
{ method: 'GET', path: '/1/classes/TestClass' },
|
|
{ method: 'GET', path: '/1/classes/TestClass' },
|
|
],
|
|
}),
|
|
});
|
|
expect(result.data.length).toEqual(3);
|
|
});
|
|
|
|
it('should include limit in error message when batch exceeds batchRequestLimit', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { batchRequestLimit: 5 },
|
|
});
|
|
await expectAsync(
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers,
|
|
body: JSON.stringify({
|
|
requests: Array.from({ length: 10 }, () => ({
|
|
method: 'GET',
|
|
path: '/1/classes/TestClass',
|
|
})),
|
|
}),
|
|
})
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
status: 400,
|
|
data: jasmine.objectContaining({
|
|
error: jasmine.stringContaining('5'),
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('subrequest path type validation', () => {
|
|
it('rejects object path in batch subrequest with proper error instead of 500', async () => {
|
|
await expectAsync(
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers,
|
|
body: JSON.stringify({
|
|
requests: [{ method: 'GET', path: { invalid: true } }],
|
|
}),
|
|
})
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({ status: 400 })
|
|
);
|
|
});
|
|
|
|
it('rejects numeric path in batch subrequest', async () => {
|
|
await expectAsync(
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers,
|
|
body: JSON.stringify({
|
|
requests: [{ method: 'GET', path: 123 }],
|
|
}),
|
|
})
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({ status: 400 })
|
|
);
|
|
});
|
|
|
|
it('rejects array path in batch subrequest', async () => {
|
|
await expectAsync(
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers,
|
|
body: JSON.stringify({
|
|
requests: [{ method: 'GET', path: ['/1/classes/Test'] }],
|
|
}),
|
|
})
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({ status: 400 })
|
|
);
|
|
});
|
|
|
|
it('rejects null path in batch subrequest', async () => {
|
|
await expectAsync(
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers,
|
|
body: JSON.stringify({
|
|
requests: [{ method: 'GET', path: null }],
|
|
}),
|
|
})
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({ status: 400 })
|
|
);
|
|
});
|
|
|
|
it('rejects boolean path in batch subrequest', async () => {
|
|
await expectAsync(
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers,
|
|
body: JSON.stringify({
|
|
requests: [{ method: 'GET', path: true }],
|
|
}),
|
|
})
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({ status: 400 })
|
|
);
|
|
});
|
|
|
|
it('still accepts valid string path in batch subrequest', async () => {
|
|
const result = await request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers,
|
|
body: JSON.stringify({
|
|
requests: [{ method: 'GET', path: '/1/classes/TestClass' }],
|
|
}),
|
|
});
|
|
expect(result.data).toEqual(jasmine.any(Array));
|
|
});
|
|
});
|
|
|
|
describe('nested batch requests', () => {
|
|
it('rejects sub-request that targets the batch endpoint', async () => {
|
|
await expectAsync(
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers,
|
|
body: JSON.stringify({
|
|
requests: [
|
|
{
|
|
method: 'POST',
|
|
path: '/1/batch',
|
|
body: {
|
|
requests: [{ method: 'GET', path: '/1/classes/TestClass' }],
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
})
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
status: 400,
|
|
data: jasmine.objectContaining({
|
|
error: 'nested batch requests are not allowed',
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('rejects when any sub-request among valid ones targets the batch endpoint', async () => {
|
|
await expectAsync(
|
|
request({
|
|
method: 'POST',
|
|
url: 'http://localhost:8378/1/batch',
|
|
headers,
|
|
body: JSON.stringify({
|
|
requests: [
|
|
{ method: 'GET', path: '/1/classes/TestClass' },
|
|
{
|
|
method: 'POST',
|
|
path: '/1/batch',
|
|
body: { requests: [{ method: 'GET', path: '/1/classes/TestClass' }] },
|
|
},
|
|
],
|
|
}),
|
|
})
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
status: 400,
|
|
data: jasmine.objectContaining({
|
|
error: 'nested batch requests are not allowed',
|
|
}),
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|