Files
Edu/services/parent-bff/test/unit/graphql-validation.test.ts
SpecialX 2229309a1e feat: initialize parent-bff service with full core features
add complete parent-bff implementation including:
- GraphQL endpoint with depth/cost validation
- ChildGuard越权校验 with redis cache and singleflight
- parallel orchestration with partial failure fallback
- three-level cache fallback strategy (Redis + LRU + downstream)
- Kafka consumer for cache invalidation and notification push
- opossum circuit breaker for downstream services
- Prometheus metrics and SLO alerts
- Helm chart for k8s deployment with multi-environment configs
- Grafana dashboard for observability
- complete unit and integration tests
2026-07-10 18:49:06 +08:00

238 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);
});
});
});