| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- /**
- * API 端点 HTTP 集成测试
- *
- * 目的:
- * - 测试 OpenAPI 端点的 HTTP 请求/响应
- * - 验证认证、权限、参数验证
- * - 测试错误处理和边界条件
- *
- * 用法:
- * bun run test:api
- *
- * 默认模式(推荐):
- * 进程内调用 Next Route Handler,无需启动开发服务器
- *
- * E2E 模式(可选):
- * 设置 API_E2E_BASE_URL 后,将改为真实 HTTP 访问(需要先启动服务与依赖)
- * 例如:API_E2E_BASE_URL=http://localhost:13500/api/actions
- */
- import { describe, expect, test } from "vitest";
- import { callActionsRoute } from "../test-utils";
- const E2E_API_BASE_URL = process.env.API_E2E_BASE_URL || "";
- const API_BASE_URL = E2E_API_BASE_URL || "http://localhost:13500/api/actions";
- // 辅助函数:发送 API 请求
- async function callApi(
- module: string,
- action: string,
- body: Record<string, unknown> = {},
- options: { authToken?: string } = {}
- ) {
- // 默认走进程内调用(稳定、无需启动服务器),仅当设置 API_E2E_BASE_URL 时才走真实 HTTP
- if (!E2E_API_BASE_URL) {
- const { response, json } = await callActionsRoute({
- method: "POST",
- pathname: `/api/actions/${module}/${action}`,
- authToken: options.authToken,
- body,
- });
- return { response, data: json as any };
- }
- const response = await fetch(`${API_BASE_URL}/${module}/${action}`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- ...(options.authToken && { Cookie: `auth-token=${options.authToken}` }),
- },
- body: JSON.stringify(body),
- });
- const data = await response.json();
- return { response, data };
- }
- describe("API 认证测试", () => {
- test("缺少 auth-token 应该返回 401", async () => {
- const { response, data } = await callApi("users", "getUsers");
- expect(response.status).toBe(401);
- expect(data.ok).toBe(false);
- expect(data.error).toContain("未认证");
- });
- test("无效的 auth-token 应该返回 401", async () => {
- // 该断言依赖数据库可用(validateKey 会查询 keys/users),因此仅在 E2E 模式运行
- if (!E2E_API_BASE_URL) {
- console.log("⚠️ 跳过无效 token 测试(需要 API_E2E_BASE_URL + 可用数据库)");
- return;
- }
- const { response, data } = await callApi(
- "users",
- "getUsers",
- {},
- { authToken: "invalid-token" }
- );
- expect(response.status).toBe(401);
- expect(data.ok).toBe(false);
- expect(data.error).toContain("认证无效");
- });
- });
- describe("API 参数验证测试", () => {
- test("缺少必需参数应该返回 400 或 500", async () => {
- if (!E2E_API_BASE_URL) {
- console.log("⚠️ 跳过参数验证测试(需要 API_E2E_BASE_URL + 可用数据库/认证)");
- return;
- }
- // 模拟登录后的 token(实际使用时需要真实 token)
- const mockToken = "test-token";
- const { response } = await callApi(
- "users",
- "editUser",
- {
- // 缺少 userId 参数
- name: "Test User",
- },
- { authToken: mockToken }
- );
- // 参数验证失败应该返回错误
- expect([400, 401, 500]).toContain(response.status);
- });
- test("无效参数类型应该返回 400 或 500", async () => {
- if (!E2E_API_BASE_URL) {
- console.log("⚠️ 跳过参数验证测试(需要 API_E2E_BASE_URL + 可用数据库/认证)");
- return;
- }
- const mockToken = "test-token";
- const { response } = await callApi(
- "keys",
- "getKeys",
- {
- userId: "not-a-number", // 应该是 number
- },
- { authToken: mockToken }
- );
- expect([400, 401, 500]).toContain(response.status);
- });
- });
- describe("API 响应格式测试", () => {
- test("所有成功响应应该符合 {ok: true, data: ...} 格式", async () => {
- // 这个测试需要真实的认证 token
- // 此处仅作示例,实际运行需要有效 session
- const mockToken = process.env.TEST_AUTH_TOKEN || "skip";
- if (mockToken === "skip") {
- console.log("⚠️ 跳过响应格式测试(需要设置 TEST_AUTH_TOKEN 环境变量)");
- return;
- }
- const { response, data } = await callApi(
- "overview",
- "getOverviewData",
- {},
- { authToken: mockToken }
- );
- if (response.ok) {
- expect(data).toHaveProperty("ok");
- expect(data.ok).toBe(true);
- expect(data).toHaveProperty("data");
- }
- });
- test("所有错误响应应该符合 {ok: false, error: ...} 格式", async () => {
- const { data } = await callApi("users", "getUsers"); // 无 auth
- expect(data).toHaveProperty("ok");
- expect(data.ok).toBe(false);
- expect(data).toHaveProperty("error");
- expect(typeof data.error).toBe("string");
- });
- });
- describe("API 端点可达性测试", () => {
- const criticalEndpoints = [
- // 用户管理
- { module: "users", action: "getUsers" },
- { module: "users", action: "addUser" },
- { module: "users", action: "editUser" },
- { module: "users", action: "removeUser" },
- // 密钥管理
- { module: "keys", action: "getKeys" },
- { module: "keys", action: "addKey" },
- // 供应商管理
- { module: "providers", action: "getProviders" },
- { module: "providers", action: "addProvider" },
- { module: "providers", action: "getProvidersHealthStatus" },
- // 统计与日志
- { module: "statistics", action: "getUserStatistics" },
- { module: "usage-logs", action: "getUsageLogs" },
- { module: "overview", action: "getOverviewData" },
- // Session 管理
- { module: "active-sessions", action: "getActiveSessions" },
- ];
- test("所有关键端点应该可访问(即使认证失败)", async () => {
- const results = await Promise.all(
- criticalEndpoints.map(async ({ module, action }) => {
- try {
- const response = !E2E_API_BASE_URL
- ? (
- await callActionsRoute({
- method: "POST",
- pathname: `/api/actions/${module}/${action}`,
- body: {},
- })
- ).response
- : await fetch(`${API_BASE_URL}/${module}/${action}`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({}),
- });
- return {
- endpoint: `${module}/${action}`,
- status: response.status,
- reachable: response.status !== 404,
- };
- } catch (error) {
- return {
- endpoint: `${module}/${action}`,
- status: 0,
- reachable: false,
- error: error instanceof Error ? error.message : String(error),
- };
- }
- })
- );
- // 所有端点都应该返回非 404 状态(401 或其他都可以)
- const unreachable = results.filter((r) => !r.reachable);
- expect(unreachable).toEqual([]);
- });
- });
- describe("API 文档 UI 可访问性", () => {
- test("Scalar UI 应该可访问", async () => {
- const response = !E2E_API_BASE_URL
- ? (await callActionsRoute({ method: "GET", pathname: "/api/actions/scalar" })).response
- : await fetch(`${API_BASE_URL}/scalar`);
- expect(response.ok).toBe(true);
- expect(response.headers.get("content-type")).toContain("text/html");
- });
- test("Swagger UI 应该可访问", async () => {
- const response = !E2E_API_BASE_URL
- ? (await callActionsRoute({ method: "GET", pathname: "/api/actions/docs" })).response
- : await fetch(`${API_BASE_URL}/docs`);
- expect(response.ok).toBe(true);
- expect(response.headers.get("content-type")).toContain("text/html");
- });
- test("健康检查端点应该正常", async () => {
- if (!E2E_API_BASE_URL) {
- // 进程内调用模式
- const { response, json } = await callActionsRoute({
- method: "GET",
- pathname: "/api/actions/health",
- });
- expect(response.ok).toBe(true);
- expect(json).toBeDefined();
- expect((json as any).status).toBe("ok");
- expect((json as any).timestamp).toBeDefined();
- expect((json as any).version).toBeDefined();
- } else {
- // E2E HTTP 调用模式
- const response = await fetch(`${API_BASE_URL}/health`);
- expect(response.ok).toBe(true);
- const data = await response.json();
- expect(data.status).toBe("ok");
- expect(data.timestamp).toBeDefined();
- expect(data.version).toBeDefined();
- }
- });
- });
|