mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
945 lines
30 KiB
JavaScript
945 lines
30 KiB
JavaScript
'use strict';
|
|
|
|
const Config = require('../lib/Config');
|
|
const auth = require('../lib/Auth');
|
|
const rest = require('../lib/rest');
|
|
|
|
describe('request complexity', () => {
|
|
function buildNestedInQuery(depth, className = '_User') {
|
|
let where = {};
|
|
for (let i = 0; i < depth; i++) {
|
|
where = { username: { $inQuery: { className, where } } };
|
|
}
|
|
return where;
|
|
}
|
|
|
|
function buildNestedNotInQuery(depth, className = '_User') {
|
|
let where = {};
|
|
for (let i = 0; i < depth; i++) {
|
|
where = { username: { $notInQuery: { className, where } } };
|
|
}
|
|
return where;
|
|
}
|
|
|
|
function buildNestedSelect(depth, className = '_User') {
|
|
let where = {};
|
|
for (let i = 0; i < depth; i++) {
|
|
where = { username: { $select: { query: { className, where }, key: 'username' } } };
|
|
}
|
|
return where;
|
|
}
|
|
|
|
function buildNestedDontSelect(depth, className = '_User') {
|
|
let where = {};
|
|
for (let i = 0; i < depth; i++) {
|
|
where = { username: { $dontSelect: { query: { className, where }, key: 'username' } } };
|
|
}
|
|
return where;
|
|
}
|
|
|
|
function buildNestedOrQuery(depth) {
|
|
let where = { username: 'test' };
|
|
for (let i = 0; i < depth; i++) {
|
|
where = { $or: [where, { username: 'test' }] };
|
|
}
|
|
return where;
|
|
}
|
|
|
|
function buildNestedAndQuery(depth) {
|
|
let where = { username: 'test' };
|
|
for (let i = 0; i < depth; i++) {
|
|
where = { $and: [where, { username: 'test' }] };
|
|
}
|
|
return where;
|
|
}
|
|
|
|
function buildNestedNorQuery(depth) {
|
|
let where = { username: 'test' };
|
|
for (let i = 0; i < depth; i++) {
|
|
where = { $nor: [where, { username: 'test' }] };
|
|
}
|
|
return where;
|
|
}
|
|
|
|
describe('config validation', () => {
|
|
it('should accept valid requestComplexity config', async () => {
|
|
await expectAsync(
|
|
reconfigureServer({
|
|
requestComplexity: {
|
|
includeDepth: 10,
|
|
includeCount: 100,
|
|
subqueryDepth: 5,
|
|
queryDepth: 10,
|
|
graphQLDepth: 15,
|
|
graphQLFields: 300,
|
|
},
|
|
})
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should accept -1 to disable a specific limit', async () => {
|
|
await expectAsync(
|
|
reconfigureServer({
|
|
requestComplexity: {
|
|
includeDepth: -1,
|
|
includeCount: -1,
|
|
subqueryDepth: -1,
|
|
queryDepth: -1,
|
|
graphQLDepth: -1,
|
|
graphQLFields: -1,
|
|
},
|
|
})
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should reject value of 0', async () => {
|
|
await expectAsync(
|
|
reconfigureServer({
|
|
requestComplexity: { includeDepth: 0 },
|
|
})
|
|
).toBeRejectedWith(
|
|
new Error('requestComplexity.includeDepth must be a positive integer or -1 to disable.')
|
|
);
|
|
});
|
|
|
|
it('should reject non-integer values', async () => {
|
|
await expectAsync(
|
|
reconfigureServer({
|
|
requestComplexity: { includeDepth: 3.5 },
|
|
})
|
|
).toBeRejectedWith(
|
|
new Error('requestComplexity.includeDepth must be a positive integer or -1 to disable.')
|
|
);
|
|
});
|
|
|
|
it('should reject non-boolean value for allowRegex', async () => {
|
|
await expectAsync(
|
|
reconfigureServer({
|
|
requestComplexity: { allowRegex: 'yes' },
|
|
})
|
|
).toBeRejectedWith(
|
|
new Error('requestComplexity.allowRegex must be a boolean.')
|
|
);
|
|
});
|
|
|
|
it('should reject unknown properties', async () => {
|
|
await expectAsync(
|
|
reconfigureServer({
|
|
requestComplexity: { unknownProp: 5 },
|
|
})
|
|
).toBeRejectedWith(
|
|
new Error("requestComplexity contains unknown property 'unknownProp'.")
|
|
);
|
|
});
|
|
|
|
it('should reject non-object values', async () => {
|
|
await expectAsync(
|
|
reconfigureServer({
|
|
requestComplexity: 'invalid',
|
|
})
|
|
).toBeRejectedWith(new Error('requestComplexity must be an object.'));
|
|
});
|
|
|
|
it('should apply defaults for missing properties', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { includeDepth: 3 },
|
|
});
|
|
const config = Config.get('test');
|
|
expect(config.requestComplexity.includeDepth).toBe(3);
|
|
expect(config.requestComplexity.includeCount).toBe(-1);
|
|
expect(config.requestComplexity.subqueryDepth).toBe(-1);
|
|
expect(config.requestComplexity.queryDepth).toBe(-1);
|
|
expect(config.requestComplexity.graphQLDepth).toBe(-1);
|
|
expect(config.requestComplexity.graphQLFields).toBe(-1);
|
|
});
|
|
|
|
it('should apply full defaults when not configured', async () => {
|
|
await reconfigureServer({});
|
|
const config = Config.get('test');
|
|
expect(config.requestComplexity).toEqual({
|
|
allowRegex: true,
|
|
batchRequestLimit: -1,
|
|
includeDepth: -1,
|
|
includeCount: -1,
|
|
subqueryDepth: -1,
|
|
subqueryLimit: -1,
|
|
queryDepth: -1,
|
|
graphQLDepth: -1,
|
|
graphQLFields: -1,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('subquery depth', () => {
|
|
let config;
|
|
|
|
beforeEach(async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { subqueryDepth: 3 },
|
|
});
|
|
config = Config.get('test');
|
|
});
|
|
|
|
it('should allow $inQuery within depth limit', async () => {
|
|
const where = buildNestedInQuery(3);
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should reject $inQuery exceeding depth limit', async () => {
|
|
const where = buildNestedInQuery(4);
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Subquery nesting depth exceeds maximum allowed depth of 3/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should reject $notInQuery exceeding depth limit', async () => {
|
|
const where = buildNestedNotInQuery(4);
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Subquery nesting depth exceeds maximum allowed depth of 3/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should reject $select exceeding depth limit', async () => {
|
|
const where = buildNestedSelect(4);
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Subquery nesting depth exceeds maximum allowed depth of 3/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should reject $dontSelect exceeding depth limit', async () => {
|
|
const where = buildNestedDontSelect(4);
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Subquery nesting depth exceeds maximum allowed depth of 3/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should allow subqueries with master key even when exceeding limit', async () => {
|
|
const where = buildNestedInQuery(4);
|
|
await expectAsync(
|
|
rest.find(config, auth.master(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should allow subqueries with maintenance key even when exceeding limit', async () => {
|
|
const where = buildNestedInQuery(4);
|
|
await expectAsync(
|
|
rest.find(config, auth.maintenance(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should allow unlimited subqueries when subqueryDepth is -1', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { subqueryDepth: -1 },
|
|
});
|
|
config = Config.get('test');
|
|
const where = buildNestedInQuery(15);
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should allow multiple sibling $inQuery at same depth within limit', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { subqueryDepth: 1 },
|
|
});
|
|
config = Config.get('test');
|
|
// Multiple sibling $inQuery operators in $or, each at depth 1 — within the limit
|
|
const where = {
|
|
$or: [
|
|
{ username: { $inQuery: { className: '_User', where: { username: 'a' } } } },
|
|
{ username: { $inQuery: { className: '_User', where: { username: 'b' } } } },
|
|
{ username: { $inQuery: { className: '_User', where: { username: 'c' } } } },
|
|
],
|
|
};
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should reject sibling $inQuery when nested beyond depth limit', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { subqueryDepth: 1 },
|
|
});
|
|
config = Config.get('test');
|
|
// Each sibling contains a nested $inQuery at depth 2 — exceeds limit
|
|
const where = {
|
|
$or: [
|
|
{
|
|
username: {
|
|
$inQuery: {
|
|
className: '_User',
|
|
where: { username: { $inQuery: { className: '_User', where: {} } } },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
username: {
|
|
$inQuery: {
|
|
className: '_User',
|
|
where: { username: { $inQuery: { className: '_User', where: {} } } },
|
|
},
|
|
},
|
|
},
|
|
],
|
|
};
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Subquery nesting depth exceeds maximum allowed depth of 1/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should allow multiple sibling $notInQuery at same depth within limit', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { subqueryDepth: 1 },
|
|
});
|
|
config = Config.get('test');
|
|
const where = {
|
|
$or: [
|
|
{ username: { $notInQuery: { className: '_User', where: { username: 'a' } } } },
|
|
{ username: { $notInQuery: { className: '_User', where: { username: 'b' } } } },
|
|
{ username: { $notInQuery: { className: '_User', where: { username: 'c' } } } },
|
|
],
|
|
};
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should allow mixed sibling $inQuery and $notInQuery at same depth within limit', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { subqueryDepth: 1 },
|
|
});
|
|
config = Config.get('test');
|
|
const where = {
|
|
$or: [
|
|
{ username: { $inQuery: { className: '_User', where: { username: 'a' } } } },
|
|
{ username: { $notInQuery: { className: '_User', where: { username: 'b' } } } },
|
|
{ username: { $inQuery: { className: '_User', where: { username: 'c' } } } },
|
|
],
|
|
};
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
});
|
|
|
|
describe('query depth', () => {
|
|
let config;
|
|
|
|
beforeEach(async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { queryDepth: 3 },
|
|
});
|
|
config = Config.get('test');
|
|
});
|
|
|
|
it('should allow $or within depth limit', async () => {
|
|
const where = buildNestedOrQuery(3);
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should reject $or exceeding depth limit', async () => {
|
|
const where = buildNestedOrQuery(4);
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth of 3/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should reject $and exceeding depth limit', async () => {
|
|
const where = buildNestedAndQuery(4);
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth of 3/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should reject $nor exceeding depth limit', async () => {
|
|
const where = buildNestedNorQuery(4);
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth of 3/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should reject mixed nested operators exceeding depth limit', async () => {
|
|
// $or > $and > $nor > $or = depth 4
|
|
const where = {
|
|
$or: [
|
|
{
|
|
$and: [
|
|
{
|
|
$nor: [
|
|
{ $or: [{ username: 'a' }, { username: 'b' }] },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth of 3/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should allow with master key even when exceeding limit', async () => {
|
|
const where = buildNestedOrQuery(4);
|
|
await expectAsync(
|
|
rest.find(config, auth.master(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should allow with maintenance key even when exceeding limit', async () => {
|
|
const where = buildNestedOrQuery(4);
|
|
await expectAsync(
|
|
rest.find(config, auth.maintenance(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should allow unlimited when queryDepth is -1', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { queryDepth: -1 },
|
|
});
|
|
config = Config.get('test');
|
|
const where = buildNestedOrQuery(15);
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
});
|
|
|
|
describe('query depth bypass via field-wrapped operators', () => {
|
|
let config;
|
|
|
|
function buildDeepOr(depth) {
|
|
let where = { username: 'test' };
|
|
for (let i = 0; i < depth; i++) {
|
|
where = { $or: [where] };
|
|
}
|
|
return where;
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { queryDepth: 3 },
|
|
});
|
|
config = Config.get('test');
|
|
});
|
|
|
|
it('should reject a deeply nested $or wrapped in $elemMatch exceeding depth limit', async () => {
|
|
const where = { username: { $elemMatch: buildDeepOr(4) } };
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth of 3/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should reject a deeply nested $or wrapped in $not exceeding depth limit', async () => {
|
|
const where = { username: { $not: buildDeepOr(4) } };
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth of 3/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should reject a deeply nested $or wrapped under a plain field name exceeding depth limit', async () => {
|
|
const where = { metadata: buildDeepOr(4) };
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Query condition nesting depth exceeds maximum allowed depth of 3/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should allow field-wrapped logical operators within depth limit', async () => {
|
|
const where = {
|
|
username: {
|
|
$inQuery: {
|
|
className: '_User',
|
|
where: { $or: [{ username: 'a' }, { username: 'b' }] },
|
|
},
|
|
},
|
|
};
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should not count field-level operators that do not nest logical operators toward depth', async () => {
|
|
const where = { username: { $in: ['a', 'b'] } };
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should not exponentially process field-wrapped deeply nested operators when queryDepth is disabled', async () => {
|
|
// With queryDepth disabled, the depth guard does not run; the walk over the
|
|
// nested $or arrays must still be linear (not O(2^n)) so a single small request
|
|
// cannot hang the event loop.
|
|
await reconfigureServer({
|
|
requestComplexity: { queryDepth: -1 },
|
|
});
|
|
config = Config.get('test');
|
|
const where = { username: { $elemMatch: buildDeepOr(26) } };
|
|
const start = Date.now();
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejected();
|
|
expect(Date.now() - start).toBeLessThan(5000);
|
|
}, 60000);
|
|
});
|
|
|
|
describe('include limits', () => {
|
|
let config;
|
|
|
|
beforeEach(async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { includeDepth: 3, includeCount: 5 },
|
|
});
|
|
config = Config.get('test');
|
|
});
|
|
|
|
it('should allow include within depth limit', async () => {
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', {}, { include: 'a.b.c' })
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should reject include exceeding depth limit', async () => {
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', {}, { include: 'a.b.c.d' })
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Include depth of 4 exceeds maximum allowed depth of 3/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should allow include count within limit', async () => {
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', {}, { include: 'a,b,c,d,e' })
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should reject include count exceeding limit', async () => {
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', {}, { include: 'a,b,c,d,e,f' })
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Number of include fields \(\d+\) exceeds maximum allowed \(5\)/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should allow includeAll when within count limit', async () => {
|
|
const schema = new Parse.Schema('IncludeTestClass');
|
|
schema.addPointer('ptr1', '_User');
|
|
schema.addPointer('ptr2', '_User');
|
|
schema.addPointer('ptr3', '_User');
|
|
await schema.save();
|
|
|
|
const obj = new Parse.Object('IncludeTestClass');
|
|
await obj.save();
|
|
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), 'IncludeTestClass', {}, { includeAll: true })
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should reject includeAll when exceeding count limit', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { includeDepth: 3, includeCount: 2 },
|
|
});
|
|
config = Config.get('test');
|
|
|
|
const schema = new Parse.Schema('IncludeTestClass2');
|
|
schema.addPointer('ptr1', '_User');
|
|
schema.addPointer('ptr2', '_User');
|
|
schema.addPointer('ptr3', '_User');
|
|
await schema.save();
|
|
|
|
const obj = new Parse.Object('IncludeTestClass2');
|
|
await obj.save();
|
|
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), 'IncludeTestClass2', {}, { includeAll: true })
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: jasmine.stringMatching(/Number of include fields .* exceeds maximum allowed/),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should allow includes with master key even when exceeding limits', async () => {
|
|
await expectAsync(
|
|
rest.find(config, auth.master(config), '_User', {}, { include: 'a.b.c.d' })
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should allow unlimited depth when includeDepth is -1', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { includeDepth: -1 },
|
|
});
|
|
config = Config.get('test');
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', {}, { include: 'a.b.c.d.e.f.g' })
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should allow unlimited count when includeCount is -1', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { includeCount: -1 },
|
|
});
|
|
config = Config.get('test');
|
|
const includes = Array.from({ length: 100 }, (_, i) => `field${i}`).join(',');
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', {}, { include: includes })
|
|
).toBeResolved();
|
|
});
|
|
});
|
|
|
|
describe('allowRegex', () => {
|
|
let config;
|
|
|
|
beforeEach(async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { allowRegex: false },
|
|
});
|
|
config = Config.get('test');
|
|
});
|
|
|
|
it('should reject $regex query when allowRegex is false (unauthenticated)', async () => {
|
|
const where = { username: { $regex: 'test' } };
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: '$regex operator is not allowed',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should reject $regex query when allowRegex is false (authenticated user)', async () => {
|
|
const user = new Parse.User();
|
|
user.setUsername('testuser');
|
|
user.setPassword('testpass');
|
|
await user.signUp();
|
|
const userAuth = new auth.Auth({
|
|
config,
|
|
isMaster: false,
|
|
user,
|
|
});
|
|
const where = { username: { $regex: 'test' } };
|
|
await expectAsync(
|
|
rest.find(config, userAuth, '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: '$regex operator is not allowed',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should allow $regex query when allowRegex is false with master key', async () => {
|
|
const where = { username: { $regex: 'test' } };
|
|
await expectAsync(
|
|
rest.find(config, auth.master(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should allow $regex query when allowRegex is true (default)', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { allowRegex: true },
|
|
});
|
|
config = Config.get('test');
|
|
const where = { username: { $regex: 'test' } };
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should reject $regex inside $or when allowRegex is false', async () => {
|
|
const where = {
|
|
$or: [
|
|
{ username: { $regex: 'test' } },
|
|
{ username: 'exact' },
|
|
],
|
|
};
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: '$regex operator is not allowed',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should reject $regex inside $and when allowRegex is false', async () => {
|
|
const where = {
|
|
$and: [
|
|
{ username: { $regex: 'test' } },
|
|
{ username: 'exact' },
|
|
],
|
|
};
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: '$regex operator is not allowed',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should reject $regex inside $nor when allowRegex is false', async () => {
|
|
const where = {
|
|
$nor: [
|
|
{ username: { $regex: 'test' } },
|
|
],
|
|
};
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: '$regex operator is not allowed',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should allow $regex by default when allowRegex is not configured', async () => {
|
|
await reconfigureServer({});
|
|
config = Config.get('test');
|
|
const where = { username: { $regex: 'test' } };
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
it('should reject empty-string $regex when allowRegex is false', async () => {
|
|
const where = { username: { $regex: '' } };
|
|
await expectAsync(
|
|
rest.find(config, auth.nobody(config), '_User', where)
|
|
).toBeRejectedWith(
|
|
jasmine.objectContaining({
|
|
message: '$regex operator is not allowed',
|
|
})
|
|
);
|
|
});
|
|
|
|
it('should allow $regex with maintenance key when allowRegex is false', async () => {
|
|
const where = { username: { $regex: 'test' } };
|
|
await expectAsync(
|
|
rest.find(config, auth.maintenance(config), '_User', where)
|
|
).toBeResolved();
|
|
});
|
|
|
|
describe('LiveQuery', () => {
|
|
beforeEach(async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { allowRegex: false },
|
|
liveQuery: { classNames: ['TestObject'] },
|
|
startLiveQueryServer: true,
|
|
});
|
|
config = Config.get('test');
|
|
});
|
|
|
|
afterEach(async () => {
|
|
const client = await Parse.CoreManager.getLiveQueryController().getDefaultLiveQueryClient();
|
|
if (client) {
|
|
await client.close();
|
|
}
|
|
});
|
|
|
|
it('should reject LiveQuery subscription with $regex when allowRegex is false', async () => {
|
|
const query = new Parse.Query('TestObject');
|
|
query.matches('field', /test/);
|
|
await expectAsync(query.subscribe()).toBeRejectedWith(
|
|
jasmine.objectContaining({ code: Parse.Error.INVALID_QUERY })
|
|
);
|
|
});
|
|
|
|
it('should reject LiveQuery subscription with $regex inside $or when allowRegex is false', async () => {
|
|
const query = new Parse.Query('TestObject');
|
|
query._where = {
|
|
$or: [
|
|
{ field: { $regex: 'test' } },
|
|
{ field: 'exact' },
|
|
],
|
|
};
|
|
await expectAsync(query.subscribe()).toBeRejectedWith(
|
|
jasmine.objectContaining({ code: Parse.Error.INVALID_QUERY })
|
|
);
|
|
});
|
|
|
|
it('should allow LiveQuery subscription without $regex when allowRegex is false', async () => {
|
|
const query = new Parse.Query('TestObject');
|
|
query.equalTo('field', 'test');
|
|
const subscription = await query.subscribe();
|
|
expect(subscription).toBeDefined();
|
|
subscription.unsubscribe();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('subquery result limit', () => {
|
|
let config;
|
|
const totalObjects = 5;
|
|
const resultLimit = 3;
|
|
|
|
beforeEach(async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { subqueryLimit: resultLimit },
|
|
});
|
|
config = Config.get('test');
|
|
// Create target objects
|
|
const targets = [];
|
|
for (let i = 0; i < totalObjects; i++) {
|
|
const obj = new Parse.Object('Target');
|
|
obj.set('value', `v${i}`);
|
|
targets.push(obj);
|
|
}
|
|
await Parse.Object.saveAll(targets);
|
|
// Create source objects, each pointing to a target
|
|
const sources = [];
|
|
for (let i = 0; i < totalObjects; i++) {
|
|
const obj = new Parse.Object('Source');
|
|
obj.set('ref', targets[i]);
|
|
obj.set('value', targets[i].get('value'));
|
|
sources.push(obj);
|
|
}
|
|
await Parse.Object.saveAll(sources);
|
|
});
|
|
|
|
it('should limit $inQuery subquery results', async () => {
|
|
const where = {
|
|
ref: {
|
|
$inQuery: { className: 'Target', where: {} },
|
|
},
|
|
};
|
|
const result = await rest.find(config, auth.nobody(config), 'Source', where);
|
|
expect(result.results.length).toBe(resultLimit);
|
|
});
|
|
|
|
it('should limit $notInQuery subquery results', async () => {
|
|
const where = {
|
|
ref: {
|
|
$notInQuery: { className: 'Target', where: {} },
|
|
},
|
|
};
|
|
const result = await rest.find(config, auth.nobody(config), 'Source', where);
|
|
// With limit, only `resultLimit` targets are excluded, so (totalObjects - resultLimit) sources remain
|
|
expect(result.results.length).toBe(totalObjects - resultLimit);
|
|
});
|
|
|
|
it('should limit $select subquery results', async () => {
|
|
const where = {
|
|
value: {
|
|
$select: { query: { className: 'Target', where: {} }, key: 'value' },
|
|
},
|
|
};
|
|
const result = await rest.find(config, auth.nobody(config), 'Source', where);
|
|
expect(result.results.length).toBe(resultLimit);
|
|
});
|
|
|
|
it('should limit $dontSelect subquery results', async () => {
|
|
const where = {
|
|
value: {
|
|
$dontSelect: { query: { className: 'Target', where: {} }, key: 'value' },
|
|
},
|
|
};
|
|
const result = await rest.find(config, auth.nobody(config), 'Source', where);
|
|
expect(result.results.length).toBe(totalObjects - resultLimit);
|
|
});
|
|
|
|
it('should allow unlimited subquery results with master key', async () => {
|
|
const where = {
|
|
ref: {
|
|
$inQuery: { className: 'Target', where: {} },
|
|
},
|
|
};
|
|
const result = await rest.find(config, auth.master(config), 'Source', where);
|
|
expect(result.results.length).toBe(totalObjects);
|
|
});
|
|
|
|
it('should allow unlimited subquery results with maintenance key', async () => {
|
|
const where = {
|
|
ref: {
|
|
$inQuery: { className: 'Target', where: {} },
|
|
},
|
|
};
|
|
const result = await rest.find(config, auth.maintenance(config), 'Source', where);
|
|
expect(result.results.length).toBe(totalObjects);
|
|
});
|
|
|
|
it('should allow unlimited subquery results when subqueryLimit is -1', async () => {
|
|
await reconfigureServer({
|
|
requestComplexity: { subqueryLimit: -1 },
|
|
});
|
|
config = Config.get('test');
|
|
const where = {
|
|
ref: {
|
|
$inQuery: { className: 'Target', where: {} },
|
|
},
|
|
};
|
|
const result = await rest.find(config, auth.nobody(config), 'Source', where);
|
|
expect(result.results.length).toBe(totalObjects);
|
|
});
|
|
|
|
it('should include subqueryLimit in config defaults', async () => {
|
|
await reconfigureServer({});
|
|
config = Config.get('test');
|
|
expect(config.requestComplexity.subqueryLimit).toBe(-1);
|
|
});
|
|
|
|
it('should accept subqueryLimit in config validation', async () => {
|
|
await expectAsync(
|
|
reconfigureServer({
|
|
requestComplexity: { subqueryLimit: 100 },
|
|
})
|
|
).toBeResolved();
|
|
});
|
|
});
|
|
});
|