mirror of
https://github.com/parse-community/parse-server
synced 2026-08-09 13:03:18 +00:00
210 lines
7.1 KiB
JavaScript
210 lines
7.1 KiB
JavaScript
'use strict';
|
||
|
||
const http = require('http');
|
||
const express = require('express');
|
||
const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
|
||
require('./helper');
|
||
const { ParseGraphQLServer } = require('../lib/GraphQL/ParseGraphQLServer');
|
||
|
||
describe('graphql query complexity', () => {
|
||
let httpServer;
|
||
let graphQLServer;
|
||
const headers = {
|
||
'X-Parse-Application-Id': 'test',
|
||
'X-Parse-Javascript-Key': 'test',
|
||
'Content-Type': 'application/json',
|
||
};
|
||
|
||
async function setupGraphQL(serverOptions = {}) {
|
||
if (httpServer) {
|
||
await new Promise(resolve => httpServer.close(resolve));
|
||
}
|
||
const server = await reconfigureServer(serverOptions);
|
||
const expressApp = express();
|
||
httpServer = http.createServer(expressApp);
|
||
expressApp.use('/parse', server.app);
|
||
graphQLServer = new ParseGraphQLServer(server, {
|
||
graphQLPath: '/graphql',
|
||
});
|
||
graphQLServer.applyGraphQL(expressApp);
|
||
await new Promise(resolve => httpServer.listen({ port: 13378 }, resolve));
|
||
}
|
||
|
||
async function graphqlRequest(query, requestHeaders = headers) {
|
||
const response = await fetch('http://localhost:13378/graphql', {
|
||
method: 'POST',
|
||
headers: requestHeaders,
|
||
body: JSON.stringify({ query }),
|
||
});
|
||
return response.json();
|
||
}
|
||
|
||
// Returns a query with depth 4: users(1) > edges(2) > node(3) > objectId(4)
|
||
function buildDeepQuery() {
|
||
return '{ users { edges { node { objectId } } } }';
|
||
}
|
||
|
||
function buildWideQuery(fieldCount) {
|
||
const fields = Array.from({ length: fieldCount }, (_, i) => `field${i}: objectId`).join('\n ');
|
||
return `{ users { edges { node { ${fields} } } } }`;
|
||
}
|
||
|
||
afterEach(async () => {
|
||
if (httpServer) {
|
||
await new Promise(resolve => httpServer.close(resolve));
|
||
httpServer = null;
|
||
}
|
||
});
|
||
|
||
describe('depth limit', () => {
|
||
it('should reject query exceeding depth limit', async () => {
|
||
await setupGraphQL({
|
||
requestComplexity: { graphQLDepth: 3 },
|
||
});
|
||
const result = await graphqlRequest(buildDeepQuery());
|
||
expect(result.errors).toBeDefined();
|
||
expect(result.errors[0].message).toMatch(
|
||
/GraphQL query depth of \d+ exceeds maximum allowed depth of 3/
|
||
);
|
||
});
|
||
|
||
it('should allow query within depth limit', async () => {
|
||
await setupGraphQL({
|
||
requestComplexity: { graphQLDepth: 10 },
|
||
});
|
||
const result = await graphqlRequest(buildDeepQuery());
|
||
expect(result.errors).toBeUndefined();
|
||
});
|
||
|
||
it('should allow deep query with master key', async () => {
|
||
await setupGraphQL({
|
||
requestComplexity: { graphQLDepth: 3 },
|
||
});
|
||
const result = await graphqlRequest(buildDeepQuery(), {
|
||
...headers,
|
||
'X-Parse-Master-Key': 'test',
|
||
});
|
||
expect(result.errors).toBeUndefined();
|
||
});
|
||
|
||
it('should allow unlimited depth when graphQLDepth is -1', async () => {
|
||
await setupGraphQL({
|
||
requestComplexity: { graphQLDepth: -1 },
|
||
});
|
||
const result = await graphqlRequest(buildDeepQuery());
|
||
expect(result.errors).toBeUndefined();
|
||
});
|
||
});
|
||
|
||
describe('fields limit', () => {
|
||
it('should reject query exceeding fields limit', async () => {
|
||
await setupGraphQL({
|
||
requestComplexity: { graphQLFields: 5 },
|
||
});
|
||
const result = await graphqlRequest(buildWideQuery(10));
|
||
expect(result.errors).toBeDefined();
|
||
expect(result.errors[0].message).toMatch(
|
||
/Number of GraphQL fields \(\d+\) exceeds maximum allowed \(5\)/
|
||
);
|
||
});
|
||
|
||
it('should allow query within fields limit', async () => {
|
||
await setupGraphQL({
|
||
requestComplexity: { graphQLFields: 200 },
|
||
});
|
||
const result = await graphqlRequest(buildDeepQuery());
|
||
expect(result.errors).toBeUndefined();
|
||
});
|
||
|
||
it('should allow wide query with master key', async () => {
|
||
await setupGraphQL({
|
||
requestComplexity: { graphQLFields: 5 },
|
||
});
|
||
const result = await graphqlRequest(buildWideQuery(10), {
|
||
...headers,
|
||
'X-Parse-Master-Key': 'test',
|
||
});
|
||
expect(result.errors).toBeUndefined();
|
||
});
|
||
|
||
it('should count fragment fields at each spread location', async () => {
|
||
// With correct counting: 2 aliases (2) + 2×edges (2) + 2×node (2) + 2×objectId from fragment (2) = 8
|
||
// With incorrect counting (fragment once): 2 + 2 + 2 + 1 = 7
|
||
// Set limit to 7 so incorrect counting passes but correct counting rejects
|
||
await setupGraphQL({
|
||
requestComplexity: { graphQLFields: 7 },
|
||
});
|
||
const result = await graphqlRequest(`
|
||
fragment UserFields on User { objectId }
|
||
{
|
||
a1: users { edges { node { ...UserFields } } }
|
||
a2: users { edges { node { ...UserFields } } }
|
||
}
|
||
`);
|
||
expect(result.errors).toBeDefined();
|
||
expect(result.errors[0].message).toMatch(
|
||
/Number of GraphQL fields \(\d+\) exceeds maximum allowed \(7\)/
|
||
);
|
||
});
|
||
|
||
it('should count inline fragment fields toward depth and field limits', async () => {
|
||
await setupGraphQL({
|
||
requestComplexity: { graphQLFields: 3 },
|
||
});
|
||
// Inline fragment adds fields without increasing depth:
|
||
// users(1) > edges(2) > ... on UserConnection { edges(3) > node(4) }
|
||
const result = await graphqlRequest(`{
|
||
users {
|
||
edges {
|
||
... on UserEdge {
|
||
node {
|
||
objectId
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}`);
|
||
expect(result.errors).toBeDefined();
|
||
expect(result.errors[0].message).toMatch(
|
||
/Number of GraphQL fields \(\d+\) exceeds maximum allowed \(3\)/
|
||
);
|
||
});
|
||
|
||
it('should allow unlimited fields when graphQLFields is -1', async () => {
|
||
await setupGraphQL({
|
||
requestComplexity: { graphQLFields: -1 },
|
||
});
|
||
const result = await graphqlRequest(buildWideQuery(50));
|
||
expect(result.errors).toBeUndefined();
|
||
});
|
||
});
|
||
|
||
describe('fragment fan-out', () => {
|
||
it('should reject query with exponential fragment fan-out efficiently', async () => {
|
||
await setupGraphQL({
|
||
requestComplexity: { graphQLFields: 100 },
|
||
});
|
||
// Binary fan-out: each fragment spreads the next one twice.
|
||
// Without fix: 2^(levels-1) field visits = 2^25 ≈ 33M (hangs event loop).
|
||
// With fix (memoization): O(levels) traversal, same field count, instant rejection.
|
||
const levels = 26;
|
||
let query = 'query Q { ...F0 }\n';
|
||
for (let i = 0; i < levels; i++) {
|
||
if (i === levels - 1) {
|
||
query += `fragment F${i} on Query { __typename }\n`;
|
||
} else {
|
||
query += `fragment F${i} on Query { ...F${i + 1} ...F${i + 1} }\n`;
|
||
}
|
||
}
|
||
const start = Date.now();
|
||
const result = await graphqlRequest(query);
|
||
const elapsed = Date.now() - start;
|
||
// Must complete in under 5 seconds (without fix it would take seconds or hang)
|
||
expect(elapsed).toBeLessThan(5000);
|
||
// Field count is 2^(levels-1) = 16777216, which exceeds the limit of 100
|
||
expect(result.errors).toBeDefined();
|
||
expect(result.errors[0].message).toMatch(/Number of GraphQL fields .* exceeds maximum allowed/);
|
||
});
|
||
});
|
||
});
|