opaque-admin-session.test.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import crypto from "node:crypto";
  2. import { beforeEach, describe, expect, it, vi } from "vitest";
  3. // Hoisted mocks
  4. const mockCookies = vi.hoisted(() => vi.fn());
  5. const mockHeaders = vi.hoisted(() => vi.fn());
  6. const mockGetEnvConfig = vi.hoisted(() => vi.fn());
  7. const mockValidateApiKeyAndGetUser = vi.hoisted(() => vi.fn());
  8. const mockFindKeyList = vi.hoisted(() => vi.fn());
  9. const mockReadSession = vi.hoisted(() => vi.fn());
  10. const mockCookieStore = vi.hoisted(() => ({
  11. get: vi.fn(),
  12. set: vi.fn(),
  13. delete: vi.fn(),
  14. }));
  15. const mockHeadersStore = vi.hoisted(() => ({
  16. get: vi.fn(),
  17. }));
  18. const mockConfig = vi.hoisted(() => ({
  19. auth: { adminToken: "test-admin-token-secret" },
  20. }));
  21. vi.mock("next/headers", () => ({
  22. cookies: mockCookies,
  23. headers: mockHeaders,
  24. }));
  25. vi.mock("@/lib/config/env.schema", () => ({
  26. getEnvConfig: mockGetEnvConfig,
  27. }));
  28. vi.mock("@/repository/key", () => ({
  29. validateApiKeyAndGetUser: mockValidateApiKeyAndGetUser,
  30. findKeyList: mockFindKeyList,
  31. }));
  32. vi.mock("@/lib/auth-session-store/redis-session-store", () => ({
  33. RedisSessionStore: class {
  34. read = mockReadSession;
  35. create = vi.fn();
  36. revoke = vi.fn();
  37. rotate = vi.fn();
  38. },
  39. }));
  40. vi.mock("@/lib/logger", () => ({
  41. logger: { warn: vi.fn(), error: vi.fn(), info: vi.fn(), debug: vi.fn() },
  42. }));
  43. vi.mock("@/lib/config/config", () => ({
  44. config: mockConfig,
  45. }));
  46. function toFingerprint(keyString: string): string {
  47. return `sha256:${crypto.createHash("sha256").update(keyString, "utf8").digest("hex")}`;
  48. }
  49. describe("opaque session with admin token (userId=-1)", () => {
  50. beforeEach(() => {
  51. vi.resetModules();
  52. vi.clearAllMocks();
  53. mockCookies.mockResolvedValue(mockCookieStore);
  54. mockHeaders.mockResolvedValue(mockHeadersStore);
  55. mockHeadersStore.get.mockReturnValue(null);
  56. mockCookieStore.get.mockReturnValue(undefined);
  57. mockGetEnvConfig.mockReturnValue({
  58. SESSION_TOKEN_MODE: "opaque",
  59. ENABLE_SECURE_COOKIES: false,
  60. });
  61. mockReadSession.mockResolvedValue(null);
  62. mockFindKeyList.mockResolvedValue([]);
  63. mockValidateApiKeyAndGetUser.mockResolvedValue(null);
  64. mockConfig.auth.adminToken = "test-admin-token-secret";
  65. });
  66. it("resolves admin session from opaque token with userId=-1", async () => {
  67. const adminToken = "test-admin-token-secret";
  68. mockCookieStore.get.mockReturnValue({ value: "sid_admin_test" });
  69. mockReadSession.mockResolvedValue({
  70. sessionId: "sid_admin_test",
  71. keyFingerprint: toFingerprint(adminToken),
  72. userId: -1,
  73. userRole: "admin",
  74. createdAt: Date.now() - 1000,
  75. expiresAt: Date.now() + 86400_000,
  76. });
  77. const { getSession } = await import("@/lib/auth");
  78. const session = await getSession();
  79. expect(session).not.toBeNull();
  80. expect(session!.user.id).toBe(-1);
  81. expect(session!.user.role).toBe("admin");
  82. expect(session!.key.name).toBe("ADMIN_TOKEN");
  83. // Must NOT call findKeyList -- virtual admin user has no DB keys
  84. expect(mockFindKeyList).not.toHaveBeenCalled();
  85. });
  86. it("returns null when admin token is not configured but session has userId=-1", async () => {
  87. mockConfig.auth.adminToken = "";
  88. mockCookieStore.get.mockReturnValue({ value: "sid_admin_test" });
  89. mockReadSession.mockResolvedValue({
  90. sessionId: "sid_admin_test",
  91. keyFingerprint: toFingerprint("test-admin-token-secret"),
  92. userId: -1,
  93. userRole: "admin",
  94. createdAt: Date.now() - 1000,
  95. expiresAt: Date.now() + 86400_000,
  96. });
  97. const { getSession } = await import("@/lib/auth");
  98. const session = await getSession();
  99. expect(session).toBeNull();
  100. expect(mockFindKeyList).not.toHaveBeenCalled();
  101. });
  102. it("returns null when fingerprint does not match admin token", async () => {
  103. mockCookieStore.get.mockReturnValue({ value: "sid_admin_test" });
  104. mockReadSession.mockResolvedValue({
  105. sessionId: "sid_admin_test",
  106. keyFingerprint: toFingerprint("wrong-token"),
  107. userId: -1,
  108. userRole: "admin",
  109. createdAt: Date.now() - 1000,
  110. expiresAt: Date.now() + 86400_000,
  111. });
  112. const { getSession } = await import("@/lib/auth");
  113. const session = await getSession();
  114. expect(session).toBeNull();
  115. expect(mockFindKeyList).not.toHaveBeenCalled();
  116. });
  117. });