api-actions-integrity.test.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. /**
  2. * Server Actions 完整性测试
  3. *
  4. * 目的:
  5. * - 验证所有 Server Actions 是否都被正确注册到 OpenAPI
  6. * - 通过 OpenAPI 文档验证端点完整性(避免直接导入 Server Actions)
  7. * - 确保没有遗漏的接口
  8. *
  9. * 用法:
  10. * bun run test:api
  11. */
  12. import { beforeAll, describe, expect, test } from "vitest";
  13. import { callActionsRoute } from "../test-utils";
  14. type OpenAPIDocument = {
  15. paths: Record<
  16. string,
  17. Record<
  18. string,
  19. {
  20. summary?: string;
  21. tags?: string[];
  22. requestBody?: {
  23. content?: {
  24. "application/json"?: {
  25. schema?: {
  26. properties?: Record<string, { type?: string; format?: string }>;
  27. };
  28. };
  29. };
  30. };
  31. responses?: {
  32. [status: string]: {
  33. content?: {
  34. "application/json"?: {
  35. schema?: {
  36. properties?: Record<string, unknown>;
  37. };
  38. };
  39. };
  40. };
  41. };
  42. }
  43. >
  44. >;
  45. };
  46. describe("OpenAPI 端点完整性检查", () => {
  47. let openApiDoc: OpenAPIDocument;
  48. beforeAll(async () => {
  49. const { response, json } = await callActionsRoute({
  50. method: "GET",
  51. pathname: "/api/actions/openapi.json",
  52. });
  53. expect(response.ok).toBe(true);
  54. openApiDoc = json as OpenAPIDocument;
  55. });
  56. test("用户管理模块的所有端点应该被注册", () => {
  57. const expectedPaths = [
  58. "/api/actions/users/getUsers",
  59. "/api/actions/users/getUsersBatch",
  60. "/api/actions/users/searchUsers",
  61. "/api/actions/users/addUser",
  62. "/api/actions/users/editUser",
  63. "/api/actions/users/removeUser",
  64. "/api/actions/users/getUserLimitUsage",
  65. ];
  66. for (const path of expectedPaths) {
  67. expect(openApiDoc.paths[path]).toBeDefined();
  68. expect(openApiDoc.paths[path].post).toBeDefined();
  69. }
  70. });
  71. test("密钥管理模块的所有端点应该被注册", () => {
  72. const expectedPaths = [
  73. "/api/actions/keys/getKeys",
  74. "/api/actions/keys/addKey",
  75. "/api/actions/keys/editKey",
  76. "/api/actions/keys/removeKey",
  77. "/api/actions/keys/getKeyLimitUsage",
  78. ];
  79. for (const path of expectedPaths) {
  80. expect(openApiDoc.paths[path]).toBeDefined();
  81. expect(openApiDoc.paths[path].post).toBeDefined();
  82. }
  83. });
  84. test("供应商管理模块的所有端点应该被注册", () => {
  85. const expectedPaths = [
  86. "/api/actions/providers/getProviders",
  87. "/api/actions/providers/addProvider",
  88. "/api/actions/providers/editProvider",
  89. "/api/actions/providers/removeProvider",
  90. "/api/actions/providers/getProvidersHealthStatus",
  91. "/api/actions/providers/resetProviderCircuit",
  92. "/api/actions/providers/getProviderLimitUsage",
  93. ];
  94. for (const path of expectedPaths) {
  95. expect(openApiDoc.paths[path]).toBeDefined();
  96. expect(openApiDoc.paths[path].post).toBeDefined();
  97. }
  98. });
  99. test("模型价格模块的所有端点应该被注册", () => {
  100. const expectedPaths = [
  101. "/api/actions/model-prices/getModelPrices",
  102. "/api/actions/model-prices/uploadPriceTable",
  103. "/api/actions/model-prices/syncLiteLLMPrices",
  104. "/api/actions/model-prices/getAvailableModelsByProviderType",
  105. "/api/actions/model-prices/hasPriceTable",
  106. ];
  107. for (const path of expectedPaths) {
  108. expect(openApiDoc.paths[path]).toBeDefined();
  109. expect(openApiDoc.paths[path].post).toBeDefined();
  110. }
  111. });
  112. test("统计分析模块的所有端点应该被注册", () => {
  113. const expectedPaths = ["/api/actions/statistics/getUserStatistics"];
  114. for (const path of expectedPaths) {
  115. expect(openApiDoc.paths[path]).toBeDefined();
  116. expect(openApiDoc.paths[path].post).toBeDefined();
  117. }
  118. });
  119. test("使用日志模块的所有端点应该被注册", () => {
  120. const expectedPaths = [
  121. "/api/actions/usage-logs/getUsageLogs",
  122. "/api/actions/usage-logs/getModelList",
  123. "/api/actions/usage-logs/getStatusCodeList",
  124. ];
  125. for (const path of expectedPaths) {
  126. expect(openApiDoc.paths[path]).toBeDefined();
  127. expect(openApiDoc.paths[path].post).toBeDefined();
  128. }
  129. });
  130. test("我的用量模块的所有端点应该被注册", () => {
  131. const expectedPaths = [
  132. "/api/actions/my-usage/getMyUsageMetadata",
  133. "/api/actions/my-usage/getMyQuota",
  134. "/api/actions/my-usage/getMyTodayStats",
  135. "/api/actions/my-usage/getMyUsageLogs",
  136. "/api/actions/my-usage/getMyUsageLogsBatch",
  137. "/api/actions/my-usage/getMyAvailableModels",
  138. "/api/actions/my-usage/getMyAvailableEndpoints",
  139. ];
  140. for (const path of expectedPaths) {
  141. expect(openApiDoc.paths[path]).toBeDefined();
  142. expect(openApiDoc.paths[path].post).toBeDefined();
  143. }
  144. });
  145. test("我的用量批量日志端点应将 cursor id 声明为整数", () => {
  146. const operation = openApiDoc.paths["/api/actions/my-usage/getMyUsageLogsBatch"]?.post;
  147. const requestCursorId = operation?.requestBody?.content?.["application/json"]?.schema
  148. ?.properties?.cursor as
  149. | { properties?: Record<string, { type?: string; format?: string }> }
  150. | undefined;
  151. const responseData = operation?.responses?.["200"]?.content?.["application/json"]?.schema
  152. ?.properties?.data as
  153. | {
  154. properties?: Record<
  155. string,
  156. { properties?: Record<string, { type?: string; format?: string }> }
  157. >;
  158. }
  159. | undefined;
  160. expect(requestCursorId?.properties?.id?.type).toBe("integer");
  161. expect(responseData?.properties?.nextCursor?.properties?.id?.type).toBe("integer");
  162. });
  163. test("概览模块的所有端点应该被注册", () => {
  164. const expectedPaths = ["/api/actions/overview/getOverviewData"];
  165. for (const path of expectedPaths) {
  166. expect(openApiDoc.paths[path]).toBeDefined();
  167. expect(openApiDoc.paths[path].post).toBeDefined();
  168. }
  169. });
  170. test("敏感词管理模块的所有端点应该被注册", () => {
  171. const expectedPaths = [
  172. "/api/actions/sensitive-words/listSensitiveWords",
  173. "/api/actions/sensitive-words/createSensitiveWordAction",
  174. "/api/actions/sensitive-words/updateSensitiveWordAction",
  175. "/api/actions/sensitive-words/deleteSensitiveWordAction",
  176. "/api/actions/sensitive-words/refreshCacheAction",
  177. "/api/actions/sensitive-words/getCacheStats",
  178. ];
  179. for (const path of expectedPaths) {
  180. expect(openApiDoc.paths[path]).toBeDefined();
  181. expect(openApiDoc.paths[path].post).toBeDefined();
  182. }
  183. });
  184. test("Session 管理模块的所有端点应该被注册", () => {
  185. const expectedPaths = [
  186. "/api/actions/active-sessions/getActiveSessions",
  187. "/api/actions/active-sessions/getSessionDetails",
  188. "/api/actions/active-sessions/getSessionMessages",
  189. ];
  190. for (const path of expectedPaths) {
  191. expect(openApiDoc.paths[path]).toBeDefined();
  192. expect(openApiDoc.paths[path].post).toBeDefined();
  193. }
  194. });
  195. test("通知管理模块的所有端点应该被注册", () => {
  196. const expectedPaths = [
  197. "/api/actions/notifications/getNotificationSettingsAction",
  198. "/api/actions/notifications/updateNotificationSettingsAction",
  199. "/api/actions/notifications/testWebhookAction",
  200. ];
  201. for (const path of expectedPaths) {
  202. expect(openApiDoc.paths[path]).toBeDefined();
  203. expect(openApiDoc.paths[path].post).toBeDefined();
  204. }
  205. });
  206. test("Webhook 目标管理模块的所有端点应该被注册", () => {
  207. const expectedPaths = [
  208. "/api/actions/webhook-targets/getWebhookTargetsAction",
  209. "/api/actions/webhook-targets/createWebhookTargetAction",
  210. "/api/actions/webhook-targets/updateWebhookTargetAction",
  211. "/api/actions/webhook-targets/deleteWebhookTargetAction",
  212. "/api/actions/webhook-targets/testWebhookTargetAction",
  213. ];
  214. for (const path of expectedPaths) {
  215. expect(openApiDoc.paths[path]).toBeDefined();
  216. expect(openApiDoc.paths[path].post).toBeDefined();
  217. }
  218. });
  219. test("通知绑定模块的所有端点应该被注册", () => {
  220. const expectedPaths = [
  221. "/api/actions/notification-bindings/getBindingsForTypeAction",
  222. "/api/actions/notification-bindings/updateBindingsAction",
  223. ];
  224. for (const path of expectedPaths) {
  225. expect(openApiDoc.paths[path]).toBeDefined();
  226. expect(openApiDoc.paths[path].post).toBeDefined();
  227. }
  228. });
  229. test("所有端点的 summary 应该非空", () => {
  230. const pathsWithoutSummary: string[] = [];
  231. for (const [path, methods] of Object.entries(openApiDoc.paths)) {
  232. for (const [method, operation] of Object.entries(methods)) {
  233. if (!operation.summary || operation.summary.trim() === "") {
  234. pathsWithoutSummary.push(`${method.toUpperCase()} ${path}`);
  235. }
  236. }
  237. }
  238. expect(pathsWithoutSummary).toEqual([]);
  239. });
  240. test("所有端点应该分配到正确的标签", () => {
  241. const pathsWithWrongTags: string[] = [];
  242. const moduleTagMapping: Record<string, string> = {
  243. "/api/actions/users/": "用户管理",
  244. "/api/actions/keys/": "密钥管理",
  245. "/api/actions/providers/": "供应商管理",
  246. "/api/actions/model-prices/": "模型价格",
  247. "/api/actions/statistics/": "统计分析",
  248. "/api/actions/usage-logs/": "使用日志",
  249. "/api/actions/overview/": "概览",
  250. "/api/actions/sensitive-words/": "敏感词管理",
  251. "/api/actions/active-sessions/": "Session 管理",
  252. "/api/actions/notifications/": "通知管理",
  253. "/api/actions/webhook-targets/": "通知管理",
  254. "/api/actions/notification-bindings/": "通知管理",
  255. };
  256. for (const [path, methods] of Object.entries(openApiDoc.paths)) {
  257. const postOperation = methods.post;
  258. if (!postOperation || !postOperation.tags) continue;
  259. // 查找对应的标签
  260. const expectedTag = Object.entries(moduleTagMapping).find(([prefix]) =>
  261. path.startsWith(prefix)
  262. )?.[1];
  263. if (expectedTag && !postOperation.tags.includes(expectedTag)) {
  264. pathsWithWrongTags.push(
  265. `${path} (期望: ${expectedTag}, 实际: ${postOperation.tags.join(", ")})`
  266. );
  267. }
  268. }
  269. expect(pathsWithWrongTags).toEqual([]);
  270. });
  271. });