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
238 lines
6.2 KiB
TypeScript
238 lines
6.2 KiB
TypeScript
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);
|
||
});
|
||
});
|
||
});
|