test-session.mjs 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. // 会话回归测试:加载实际请求层和 store,以可控 HTTP 响应验证过期恢复,避免时间等待造成不稳定。
  2. import assert from "node:assert/strict";
  3. import { readFile } from "node:fs/promises";
  4. import ts from "typescript";
  5. import { createPinia, setActivePinia } from "pinia";
  6. const storage = new Map();
  7. globalThis.sessionStorage = {
  8. getItem: (key) => storage.get(key) ?? null,
  9. setItem: (key, value) => storage.set(key, value),
  10. removeItem: (key) => storage.delete(key),
  11. };
  12. let expired = 0;
  13. globalThis.window = {
  14. dispatchEvent: (event) => {
  15. if (event.type === "session-expired") expired++;
  16. },
  17. };
  18. // Node 不提供 Vite 的 import.meta.env;只替换构建时基地址,不改动被测函数。
  19. const asModule = (source) =>
  20. "data:text/javascript;base64," +
  21. Buffer.from(
  22. ts.transpileModule(source, {
  23. compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ES2022 },
  24. }).outputText,
  25. ).toString("base64");
  26. const apiSource = await readFile(new URL("../src/api.ts", import.meta.url), "utf8");
  27. const apiUrl = asModule(apiSource.replace("import.meta.env.VITE_API_BASE", '"/api/v1"'));
  28. const api = await import(apiUrl);
  29. const storeSource = await readFile(new URL("../src/store.ts", import.meta.url), "utf8");
  30. const { useSession } = await import(
  31. asModule(
  32. storeSource
  33. .replace('from "./api"', `from ${JSON.stringify(apiUrl)}`)
  34. .replace('from "pinia"', `from ${JSON.stringify(import.meta.resolve("pinia"))}`),
  35. )
  36. );
  37. setActivePinia(createPinia());
  38. const session = useSession();
  39. const oldTokens = { access_token: "expired-test-access", refresh_token: "old-test-refresh" };
  40. const newTokens = { access_token: "renewed-test-access", refresh_token: "rotated-test-refresh" };
  41. const result = (status, data) =>
  42. Response.json(status < 400 ? { data, message: "ok", request_id: "test" } : {
  43. error: { code: "UNAUTHORIZED", message: "测试过期凭据" }, request_id: "test",
  44. }, { status });
  45. api.saveTokens(oldTokens);
  46. let refreshes = 0;
  47. globalThis.fetch = async (url, options = {}) => {
  48. if (url.endsWith("/auth/refresh")) {
  49. refreshes++;
  50. assert.equal(JSON.parse(options.body).refresh_token, oldTokens.refresh_token);
  51. return result(200, newTokens);
  52. }
  53. return options.headers.get("Authorization") === `Bearer ${newTokens.access_token}`
  54. ? result(200, { id: 1, role: "employee" }) : result(401);
  55. };
  56. await session.load();
  57. assert.equal(session.user.id, 1);
  58. assert.equal(refreshes, 1);
  59. assert.equal(api.tokens().refresh_token, newTokens.refresh_token);
  60. console.log("PASS /auth/me 过期时自动刷新并恢复用户");
  61. api.saveTokens(oldTokens);
  62. refreshes = 0;
  63. await Promise.all([api.api("/audit"), api.api("/dashboard/stats")]);
  64. assert.equal(refreshes, 1);
  65. console.log("PASS 并发请求只轮换一次刷新令牌");
  66. api.saveTokens(oldTokens);
  67. refreshes = 0;
  68. await assert.rejects(api.api("/auth/login", "POST", { username: "test", password: "test" }), (error) => error.status === 401);
  69. assert.equal(refreshes, 0);
  70. console.log("PASS 登录失败不触发递归刷新");
  71. api.saveTokens(oldTokens);
  72. let revoked = false;
  73. globalThis.fetch = async (url, options = {}) => {
  74. if (url.endsWith("/auth/refresh")) return result(200, newTokens);
  75. if (url.endsWith("/auth/logout")) {
  76. assert.equal(JSON.parse(options.body).refresh_token, newTokens.refresh_token);
  77. assert.equal(options.headers.get("Authorization"), `Bearer ${newTokens.access_token}`);
  78. revoked = true;
  79. return result(200, { revoked: true });
  80. }
  81. return options.headers.get("Authorization") === `Bearer ${newTokens.access_token}`
  82. ? result(200, { id: 1, role: "employee" }) : result(401);
  83. };
  84. await session.logout();
  85. assert.equal(revoked, true);
  86. assert.equal(api.tokens().access_token, undefined);
  87. assert.equal(session.user, null);
  88. console.log("PASS 退出撤销轮换后的令牌并清理本地会话");
  89. api.saveTokens(oldTokens);
  90. expired = 0;
  91. let calls = 0;
  92. globalThis.fetch = async () => { calls++; return result(401); };
  93. await assert.rejects(api.api("/auth/me"), (error) => error.status === 401);
  94. assert.equal(calls, 2);
  95. assert.equal(expired, 1);
  96. assert.equal(api.tokens().access_token, undefined);
  97. console.log("PASS 刷新失败终止重试并通知登录页面");