users-keys-complete.test.ts 18 KB

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