2
0

users-keys-complete.test.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586
  1. /**
  2. * 用户和 API Key 管理完整 E2E 测试
  3. *
  4. * 📋 测试流程:
  5. * 1. 创建测试用户
  6. * 2. 为用户创建 API Key
  7. * 3. 测试 Key 的查询、管理
  8. * 4. 测试用户的编辑、禁用/启用
  9. * 5. 清理测试数据
  10. *
  11. * 🔑 认证方式:
  12. * - 使用 Cookie: auth-token
  13. * - Token 从环境变量读取(ADMIN_TOKEN)
  14. *
  15. * ⚙️ 前提条件:
  16. * - 开发服务器运行在 http://localhost:13500
  17. * - PostgreSQL 和 Redis 已启动
  18. * - ADMIN_TOKEN 已配置在 .env 文件中
  19. *
  20. * 🧹 数据清理:
  21. * - 测试完成后自动清理所有创建的用户和 Key
  22. * - 使用 afterAll 钩子确保清理执行
  23. */
  24. import { afterAll, beforeAll, describe, expect, test } from "vitest";
  25. // ==================== 配置 ====================
  26. /** API 基础 URL */
  27. const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:13500/api/actions";
  28. /** 管理员认证 Token(从环境变量读取)*/
  29. const ADMIN_TOKEN = process.env.TEST_ADMIN_TOKEN || process.env.ADMIN_TOKEN;
  30. /** 测试数据存储(用于清理)*/
  31. const testData = {
  32. /** 创建的用户 ID 列表 */
  33. userIds: [] as number[],
  34. /** 创建的 Key ID 列表 */
  35. keyIds: [] as number[],
  36. };
  37. // ==================== 辅助函数 ====================
  38. /**
  39. * 调用 API 端点
  40. *
  41. * @param module - 模块名(如 "users", "keys")
  42. * @param action - 操作名(如 "getUsers", "addUser")
  43. * @param body - 请求体参数
  44. * @param authToken - 认证 Token(默认使用 ADMIN_TOKEN)
  45. * @returns Promise<{response: Response, data: any}>
  46. *
  47. * @example
  48. * const { response, data } = await callApi("users", "getUsers");
  49. */
  50. async function callApi(
  51. module: string,
  52. action: string,
  53. body: Record<string, unknown> = {},
  54. authToken = ADMIN_TOKEN
  55. ) {
  56. const url = `${API_BASE_URL}/${module}/${action}`;
  57. const response = await fetch(url, {
  58. method: "POST",
  59. headers: {
  60. "Content-Type": "application/json",
  61. Cookie: `auth-token=${authToken}`,
  62. },
  63. body: JSON.stringify(body),
  64. });
  65. // 检查响应是否是 JSON
  66. const contentType = response.headers.get("content-type");
  67. if (contentType?.includes("application/json")) {
  68. const data = await response.json();
  69. return { response, data };
  70. }
  71. // 非 JSON 响应,返回文本
  72. const text = await response.text();
  73. return { response, data: { ok: false, error: `非JSON响应: ${text}` } };
  74. }
  75. /**
  76. * 期望 API 调用成功
  77. *
  78. * 验证:
  79. * - HTTP 状态码为 200
  80. * - 响应格式为 {ok: true, data: ...}(data 可能为 null)
  81. *
  82. * @returns data 字段的内容(可能为 null)
  83. *
  84. * @example
  85. * const user = await expectSuccess("users", "addUser", { name: "测试" });
  86. */
  87. async function expectSuccess(module: string, action: string, body: Record<string, unknown> = {}) {
  88. const { response, data } = await callApi(module, action, body);
  89. // 验证 HTTP 状态码
  90. expect(response.status).toBe(200);
  91. expect(response.ok).toBe(true);
  92. // 验证响应格式
  93. expect(data).toHaveProperty("ok");
  94. expect(data.ok).toBe(true);
  95. // data 字段可能不存在(某些操作只返回 {ok: true})
  96. return data.data;
  97. }
  98. /**
  99. * 期望 API 调用失败
  100. *
  101. * 验证:
  102. * - HTTP 状态码为 400(业务逻辑错误)或 401/403(认证/权限错误)
  103. * - 响应格式为 {ok: false, error: "..."} 或 Zod 验证错误格式 {success: false, error: {...}}
  104. *
  105. * @returns error 错误消息
  106. *
  107. * @example
  108. * const error = await expectError("users", "addUser", { name: "" });
  109. * expect(error).toContain("用户名");
  110. */
  111. async function expectError(module: string, action: string, body: Record<string, unknown> = {}) {
  112. const { response, data } = await callApi(module, action, body);
  113. // API 返回 400/401/403 状态码,表示业务错误或权限问题
  114. expect([400, 401, 403].includes(response.status)).toBe(true);
  115. // 验证错误响应格式(支持两种格式)
  116. if (data.ok !== undefined) {
  117. // 标准格式:{ok: false, error: "..."}
  118. expect(data.ok).toBe(false);
  119. expect(data).toHaveProperty("error");
  120. return data.error;
  121. } else if (data.success !== undefined) {
  122. // Zod 验证错误格式:{success: false, error: {...}}
  123. expect(data.success).toBe(false);
  124. expect(data).toHaveProperty("error");
  125. // 提取 Zod 错误消息
  126. const zodError = data.error;
  127. if (zodError.issues && Array.isArray(zodError.issues)) {
  128. return zodError.issues.map((issue: any) => issue.message).join("; ");
  129. }
  130. return JSON.stringify(zodError);
  131. } else {
  132. throw new Error(`未知的错误响应格式: ${JSON.stringify(data)}`);
  133. }
  134. }
  135. // ==================== 测试清理 ====================
  136. /**
  137. * 测试完成后清理所有创建的数据
  138. *
  139. * 清理顺序:
  140. * 1. 删除所有创建的 Keys
  141. * 2. 删除所有创建的用户
  142. */
  143. afterAll(async () => {
  144. console.log("\n🧹 开始清理 E2E 测试数据...");
  145. console.log(` 用户数:${testData.userIds.length}`);
  146. console.log(` Key数:${testData.keyIds.length}`);
  147. // 清理用户(会自动清理关联的 Keys)
  148. for (const userId of testData.userIds) {
  149. try {
  150. await callApi("users", "removeUser", { userId });
  151. } catch (_error) {
  152. console.warn(`⚠️ 清理用户 ${userId} 失败`);
  153. }
  154. }
  155. console.log("✅ E2E 测试数据清理完成\n");
  156. });
  157. // ==================== 测试套件 ====================
  158. describe("用户和 Key 管理 - 完整 E2E 测试", () => {
  159. // 测试用户 ID(在多个测试间共享)
  160. let testUser1Id: number;
  161. let testUser2Id: number;
  162. // ==================== 第1部分:用户管理 ====================
  163. describe("【用户管理】创建和查询", () => {
  164. test("1.1 应该成功创建第一个用户", async () => {
  165. const result = await expectSuccess("users", "addUser", {
  166. name: `E2E用户1_${Date.now()}`,
  167. note: "E2E测试用户1",
  168. rpm: 100,
  169. dailyQuota: 50,
  170. isEnabled: true,
  171. });
  172. // 验证返回结构
  173. expect(result).toHaveProperty("user");
  174. expect(result).toHaveProperty("defaultKey");
  175. // 验证用户信息
  176. expect(result.user.name).toContain("E2E用户1");
  177. expect(result.user.rpm).toBe(100);
  178. expect(result.user.dailyQuota).toBe(50);
  179. // 验证默认 Key
  180. expect(result.defaultKey.key).toMatch(/^sk-[a-f0-9]{32}$/);
  181. // 保存用户 ID 和 Key ID
  182. testUser1Id = result.user.id;
  183. testData.userIds.push(testUser1Id);
  184. console.log(`✅ 创建用户1成功 (ID: ${testUser1Id})`);
  185. });
  186. test("1.2 应该成功创建第二个用户(带完整限额)", async () => {
  187. const result = await expectSuccess("users", "addUser", {
  188. name: `E2E用户2_${Date.now()}`,
  189. note: "E2E测试用户2 - 高级配置",
  190. rpm: 200,
  191. dailyQuota: 100,
  192. limit5hUsd: 50,
  193. limitWeeklyUsd: 300,
  194. limitMonthlyUsd: 1000,
  195. limitConcurrentSessions: 10,
  196. tags: ["test", "premium"],
  197. isEnabled: true,
  198. });
  199. testUser2Id = result.user.id;
  200. testData.userIds.push(testUser2Id);
  201. // 验证高级配置
  202. // API 返回的金额字段是字符串格式(Decimal.js)
  203. expect(parseFloat(result.user.limit5hUsd)).toBe(50);
  204. expect(parseFloat(result.user.limitWeeklyUsd)).toBe(300);
  205. expect(result.user.tags).toContain("premium");
  206. console.log(`✅ 创建用户2成功 (ID: ${testUser2Id})`);
  207. });
  208. test("1.3 应该能查询到创建的用户", async () => {
  209. const users = await expectSuccess("users", "getUsers");
  210. expect(Array.isArray(users)).toBe(true);
  211. expect(users.length).toBeGreaterThanOrEqual(2);
  212. // 验证用户1存在
  213. const user1 = users.find((u: any) => u.id === testUser1Id);
  214. expect(user1).toBeDefined();
  215. expect(user1.name).toContain("E2E用户1");
  216. // 验证用户2存在
  217. const user2 = users.find((u: any) => u.id === testUser2Id);
  218. expect(user2).toBeDefined();
  219. expect(user2.name).toContain("E2E用户2");
  220. });
  221. });
  222. describe("【用户管理】编辑和状态管理", () => {
  223. test("2.1 应该成功编辑用户信息", async () => {
  224. const _result = await expectSuccess("users", "editUser", {
  225. userId: testUser1Id,
  226. name: `E2E用户1_已编辑_${Date.now()}`,
  227. note: "已修改",
  228. rpm: 150,
  229. dailyQuota: 80,
  230. });
  231. // editUser 返回 null,需要重新查询验证
  232. const users = await expectSuccess("users", "getUsers");
  233. const updatedUser = users.find((u: any) => u.id === testUser1Id);
  234. expect(updatedUser.name).toContain("已编辑");
  235. expect(updatedUser.rpm).toBe(150);
  236. });
  237. test("2.2 应该成功禁用用户", async () => {
  238. await expectSuccess("users", "editUser", {
  239. userId: testUser1Id,
  240. name: `E2E用户1_${Date.now()}`, // 必填字段
  241. isEnabled: false,
  242. });
  243. // 验证用户已禁用
  244. const users = await expectSuccess("users", "getUsers");
  245. const user = users.find((u: any) => u.id === testUser1Id);
  246. expect(user.isEnabled).toBe(false);
  247. });
  248. test("2.3 应该成功启用用户", async () => {
  249. await expectSuccess("users", "editUser", {
  250. userId: testUser1Id,
  251. name: `E2E用户1_${Date.now()}`, // 必填字段
  252. isEnabled: true,
  253. });
  254. // 验证用户已启用
  255. const users = await expectSuccess("users", "getUsers");
  256. const user = users.find((u: any) => u.id === testUser1Id);
  257. expect(user.isEnabled).toBe(true);
  258. });
  259. });
  260. // ==================== 第2部分:API Key 管理 ====================
  261. describe("【Key 管理】创建和查询", () => {
  262. test("3.1 应该能获取用户的 Keys(包含默认 Key)", async () => {
  263. const keys = await expectSuccess("keys", "getKeys", {
  264. userId: testUser1Id,
  265. });
  266. expect(Array.isArray(keys)).toBe(true);
  267. expect(keys.length).toBeGreaterThanOrEqual(1); // 至少有默认 Key
  268. // 验证 Key 结构
  269. const key = keys[0];
  270. expect(key).toHaveProperty("id");
  271. expect(key).toHaveProperty("userId");
  272. expect(key).toHaveProperty("key");
  273. expect(key).toHaveProperty("name");
  274. // 验证 Key 格式(getKeys 返回完整 key,不是脱敏格式)
  275. expect(key.key).toMatch(/^sk-[a-f0-9]{32}$/);
  276. });
  277. test("3.2 应该成功为用户创建新 Key", async () => {
  278. const result = await expectSuccess("keys", "addKey", {
  279. userId: testUser1Id,
  280. name: `E2E测试Key_${Date.now()}`,
  281. });
  282. // 验证返回格式(根据实际 API)
  283. expect(result).toHaveProperty("generatedKey");
  284. expect(result).toHaveProperty("name");
  285. // 验证 Key 格式
  286. expect(result.generatedKey).toMatch(/^sk-[a-f0-9]{32}$/);
  287. console.log(`✅ 创建 Key 成功: ${result.name}`);
  288. });
  289. test("3.3 应该成功创建带限额的 Key", async () => {
  290. const result = await expectSuccess("keys", "addKey", {
  291. userId: testUser2Id,
  292. name: `E2E限额Key_${Date.now()}`,
  293. limitDailyUsd: 5,
  294. limit5hUsd: 10,
  295. limitWeeklyUsd: 50,
  296. limitMonthlyUsd: 200,
  297. });
  298. expect(result.generatedKey).toMatch(/^sk-[a-f0-9]{32}$/);
  299. console.log(`✅ 创建限额 Key 成功: ${result.name}`);
  300. });
  301. test("3.4 应该拒绝为不存在的用户创建 Key", async () => {
  302. const error = await expectError("keys", "addKey", {
  303. userId: 999999,
  304. name: "无效用户的Key",
  305. });
  306. expect(error).toBeDefined();
  307. expect(typeof error).toBe("string");
  308. });
  309. });
  310. describe("【Key 管理】删除操作", () => {
  311. let tempUserId: number;
  312. let tempKeyId: number;
  313. beforeAll(async () => {
  314. // 创建临时用户用于测试 Key 删除
  315. const userResult = await expectSuccess("users", "addUser", {
  316. name: `E2E临时用户_${Date.now()}`,
  317. rpm: 60,
  318. dailyQuota: 10,
  319. });
  320. tempUserId = userResult.user.id;
  321. testData.userIds.push(tempUserId);
  322. // 创建额外的 Key
  323. const _keyResult = await expectSuccess("keys", "addKey", {
  324. userId: tempUserId,
  325. name: `临时Key_${Date.now()}`,
  326. });
  327. // 获取 Key ID(需要查询 getKeys)
  328. const keys = await expectSuccess("keys", "getKeys", { userId: tempUserId });
  329. const createdKey = keys.find((k: any) => k.name.includes("临时Key"));
  330. tempKeyId = createdKey.id;
  331. });
  332. test("4.1 应该成功删除 Key", async () => {
  333. // 删除刚创建的 Key
  334. await expectSuccess("keys", "removeKey", { keyId: tempKeyId });
  335. // 验证 Key 已被删除
  336. const keys = await expectSuccess("keys", "getKeys", { userId: tempUserId });
  337. const deletedKey = keys.find((k: any) => k.id === tempKeyId);
  338. expect(deletedKey).toBeUndefined();
  339. console.log(`✅ 删除 Key ${tempKeyId} 成功`);
  340. });
  341. test("4.2 应该拒绝删除不存在的 Key", async () => {
  342. const error = await expectError("keys", "removeKey", {
  343. keyId: 999999,
  344. });
  345. expect(error).toBeDefined();
  346. });
  347. test("4.3 应该拒绝删除用户的最后一个 Key", async () => {
  348. // 获取剩余的 Keys
  349. const keys = await expectSuccess("keys", "getKeys", { userId: tempUserId });
  350. expect(keys.length).toBe(1); // 只剩默认 Key
  351. const lastKeyId = keys[0].id;
  352. // 尝试删除最后一个 Key
  353. const error = await expectError("keys", "removeKey", {
  354. keyId: lastKeyId,
  355. });
  356. expect(error).toBeDefined();
  357. expect(error).toContain("至少");
  358. });
  359. });
  360. // ==================== 第3部分:参数验证 ====================
  361. describe("【参数验证】边界条件测试", () => {
  362. test("5.1 创建用户 - 应该拒绝空用户名", async () => {
  363. const error = await expectError("users", "addUser", {
  364. name: "",
  365. rpm: 60,
  366. dailyQuota: 10,
  367. });
  368. expect(error).toBeDefined();
  369. });
  370. test("5.2 创建用户 - 应该拒绝无效的 RPM", async () => {
  371. const error = await expectError("users", "addUser", {
  372. name: "测试",
  373. rpm: -1, // 负数无效,0 表示无限制
  374. dailyQuota: 10,
  375. });
  376. expect(error).toBeDefined();
  377. });
  378. test("5.3 创建用户 - 应该拒绝负数配额", async () => {
  379. const error = await expectError("users", "addUser", {
  380. name: "测试",
  381. rpm: 60,
  382. dailyQuota: -10, // 负数
  383. });
  384. expect(error).toBeDefined();
  385. });
  386. test("5.4 编辑用户 - 幂等操作(编辑不存在的用户也返回成功)", async () => {
  387. // 注意:editUser 对不存在的用户是幂等操作,不会报错
  388. // 这与 removeUser 的行为一致
  389. const { response, data } = await callApi("users", "editUser", {
  390. userId: 999999,
  391. name: "不存在",
  392. });
  393. // 验证返回成功(幂等操作)
  394. expect(response.ok).toBe(true);
  395. expect(data.ok).toBe(true);
  396. });
  397. test("5.5 删除用户 - 幂等操作(删除不存在的用户也返回成功)", async () => {
  398. // 删除不存在的用户是幂等操作,返回 {ok: true}
  399. await expectSuccess("users", "removeUser", {
  400. userId: 999999,
  401. });
  402. // 不验证 result,因为可能为 null/undefined
  403. });
  404. });
  405. // ==================== 第4部分:完整流程测试 ====================
  406. describe("【完整流程】用户生命周期", () => {
  407. test("6.1 完整流程:创建→编辑→禁用→启用→删除", async () => {
  408. // Step 1: 创建用户
  409. const createResult = await expectSuccess("users", "addUser", {
  410. name: `E2E流程测试_${Date.now()}`,
  411. rpm: 60,
  412. dailyQuota: 10,
  413. });
  414. const userId = createResult.user.id;
  415. const originalName = createResult.user.name;
  416. console.log(` Step 1: 创建用户 ${userId} ✅`);
  417. // Step 2: 编辑用户
  418. const editedName = `${originalName}_已编辑`;
  419. await expectSuccess("users", "editUser", {
  420. userId,
  421. name: editedName,
  422. rpm: 120,
  423. dailyQuota: 20,
  424. });
  425. console.log(` Step 2: 编辑用户 ✅`);
  426. // Step 3: 禁用用户
  427. await expectSuccess("users", "editUser", {
  428. userId,
  429. name: editedName, // 保持相同的名称
  430. isEnabled: false,
  431. });
  432. console.log(` Step 3: 禁用用户 ✅`);
  433. // Step 4: 启用用户
  434. await expectSuccess("users", "editUser", {
  435. userId,
  436. name: editedName, // 保持相同的名称
  437. isEnabled: true,
  438. });
  439. console.log(` Step 4: 启用用户 ✅`);
  440. // Step 5: 删除用户
  441. await expectSuccess("users", "removeUser", { userId });
  442. // 验证用户已删除
  443. const users = await expectSuccess("users", "getUsers");
  444. const deletedUser = users.find((u: any) => u.id === userId);
  445. expect(deletedUser).toBeUndefined();
  446. console.log(` Step 5: 删除用户 ✅`);
  447. console.log(` ✅ 完整流程测试通过`);
  448. });
  449. test("6.2 完整流程:创建用户→创建多个Key→删除Key→删除用户", async () => {
  450. // Step 1: 创建用户
  451. const userResult = await expectSuccess("users", "addUser", {
  452. name: `E2E多Key测试_${Date.now()}`,
  453. rpm: 60,
  454. dailyQuota: 10,
  455. });
  456. const userId = userResult.user.id;
  457. testData.userIds.push(userId);
  458. console.log(` Step 1: 创建用户 ${userId} ✅`);
  459. // Step 2: 创建3个额外的 Key
  460. const createdKeys = [];
  461. for (let i = 1; i <= 3; i++) {
  462. const _keyResult = await expectSuccess("keys", "addKey", {
  463. userId,
  464. name: `测试Key${i}_${Date.now()}`,
  465. });
  466. createdKeys.push(_keyResult);
  467. console.log(` Step 2.${i}: 创建Key${i} ✅`);
  468. }
  469. // Step 3: 获取所有 Keys(应该有4个:1个默认 + 3个新建)
  470. const keys = await expectSuccess("keys", "getKeys", { userId });
  471. expect(keys.length).toBe(4);
  472. console.log(` Step 3: 验证 Key 数量(4个)✅`);
  473. // Step 4: 删除用户(会自动删除所有 Keys)
  474. await expectSuccess("users", "removeUser", { userId });
  475. console.log(` Step 4: 删除用户及所有 Keys ✅`);
  476. console.log(` ✅ 多Key流程测试通过`);
  477. });
  478. });
  479. });