chore(parent-bff): merge parent-bff module into main
Merge feat/parent-bff-ai05 into main, conflicts resolved in favor of feature branch
This commit is contained in:
239
services/parent-bff/test/unit/graphql-validation.test.ts
Normal file
239
services/parent-bff/test/unit/graphql-validation.test.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user