import { describe, it, expect } from "vitest"; import { validate, parse, GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLBoolean, GraphQLID, GraphQLList, GraphQLNonNull, } from "graphql"; import depthLimit from "graphql-depth-limit"; import { createComplexityRule, simpleEstimator, } from "graphql-query-complexity"; const DEPTH_LIMIT = 7; const COST_LIMIT = 1000; /** * 测试用 schema:包含递归类型以测试深度限制。 * * 直接使用 graphql 包的 GraphQLSchema 构造,避免 * @graphql-tools/schema 引入不同 graphql 实例导致 * "Cannot use GraphQLSchema from another module or realm" 错误。 */ const typeA: GraphQLObjectType = new GraphQLObjectType({ name: "A", fields: () => ({ a: { type: typeA }, value: { type: GraphQLString }, }), }); const parentType = new GraphQLObjectType({ name: "Parent", fields: { id: { type: new GraphQLNonNull(GraphQLID) }, name: { type: new GraphQLNonNull(GraphQLString) }, }, }); const childType = new GraphQLObjectType({ name: "Child", fields: { id: { type: new GraphQLNonNull(GraphQLID) }, name: { type: new GraphQLNonNull(GraphQLString) }, }, }); const dashboardDataType = new GraphQLObjectType({ name: "DashboardData", fields: { parent: { type: parentType }, children: { type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(childType))) }, degraded: { type: new GraphQLNonNull(GraphQLBoolean) }, }, }); const queryType = new GraphQLObjectType({ name: "Query", fields: { a: { type: typeA }, dashboard: { type: dashboardDataType }, }, }); const testSchema = new GraphQLSchema({ query: queryType }); function validateQuery( query: string, opts: { depth?: number; cost?: number } = {}, ): ReturnType { const rules: ReturnType[] = []; if (opts.depth !== undefined) { rules.push(depthLimit(opts.depth)); } if (opts.cost !== undefined) { rules.push( createComplexityRule({ maximumComplexity: opts.cost, estimators: [simpleEstimator({ defaultComplexity: 1 })], onComplete: () => {}, }), ); } return validate(testSchema, parse(query), rules); } describe("GraphQL 查询复杂度限制(测试用例 6)", () => { describe("深度限制", () => { it("depth=7 查询通过", () => { const query = `query { a { # 1 a { # 2 a { # 3 a { # 4 a { # 5 a { # 6 a { # 7 value } } } } } } } }`; const errors = validateQuery(query, { depth: DEPTH_LIMIT }); expect(errors).toHaveLength(0); }); it("depth=8 查询被拒", () => { const query = `query { a { # 1 a { # 2 a { # 3 a { # 4 a { # 5 a { # 6 a { # 7 a { # 8 value } } } } } } } } }`; const errors = validateQuery(query, { depth: DEPTH_LIMIT }); expect(errors.length).toBeGreaterThan(0); expect(errors[0]!.message).toContain("depth"); }); it("depth=1 查询通过", () => { const query = `query { a { value } }`; const errors = validateQuery(query, { depth: DEPTH_LIMIT }); expect(errors).toHaveLength(0); }); }); describe("复杂度限制", () => { it("低复杂度查询通过", () => { const query = `query { dashboard { degraded parent { id name } children { id name } } }`; const errors = validateQuery(query, { cost: COST_LIMIT }); expect(errors).toHaveLength(0); }); it("cost > 1000 查询被拒", () => { const aliases: string[] = []; for (let i = 0; i < 501; i++) { aliases.push(`a${i}: dashboard { degraded }`); } const query = `query { ${aliases.join(" ")} }`; const errors = validateQuery(query, { cost: COST_LIMIT }); expect(errors.length).toBeGreaterThan(0); expect(errors[0]!.message).toContain("complexity"); }); it("cost = 1000 查询通过(边界值)", () => { const aliases: string[] = []; for (let i = 0; i < 500; i++) { aliases.push(`a${i}: dashboard { degraded }`); } const query = `query { ${aliases.join(" ")} }`; const errors = validateQuery(query, { cost: COST_LIMIT }); expect(errors).toHaveLength(0); }); }); describe("组合限制", () => { it("同时满足深度和复杂度限制的查询通过", () => { const query = `query { a { # 1 a { # 2 a { # 3 value } } } dashboard { degraded parent { id } } }`; const errors = validateQuery(query, { depth: DEPTH_LIMIT, cost: COST_LIMIT, }); expect(errors).toHaveLength(0); }); it("深度超限但复杂度未超限时被深度规则拒绝", () => { const query = `query { a { # 1 a { # 2 a { # 3 a { # 4 a { # 5 a { # 6 a { # 7 a { # 8 value } } } } } } } } }`; const errors = validateQuery(query, { depth: DEPTH_LIMIT, cost: COST_LIMIT, }); const depthErrors = errors.filter((e) => e.message.toLowerCase().includes("depth"), ); expect(depthErrors.length).toBeGreaterThan(0); }); }); });