Files
Edu/services/parent-bff/test/unit/graphql-validation.test.ts
SpecialX 8f21ca6a68 chore(parent-bff): merge parent-bff module into main
Merge feat/parent-bff-ai05 into main, conflicts resolved in favor of feature branch
2026-07-10 19:00:29 +08:00

240 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<typeof validate> {
const rules: ReturnType<typeof depthLimit>[] = [];
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);
});
});
});