import { SESSION_SERVER_API_URL } from "@toruslabs/constants"; import { Hex } from "@toruslabs/metadata-helpers"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { BaseSessionManager } from "../src/base"; import { SessionManager } from "../src/sessionManager"; describe("SessionManager", () => { describe("Input Validation", () => { test("should throw error if sessionId is not set", async () => { const sessionManager = new SessionManager({}); await expect(sessionManager.createSession({})).rejects.toThrow("Session id is required"); }); test("should use default base url if not set", () => { const sessionManager = new SessionManager({}); expect(sessionManager.sessionServerBaseUrl).toBe(SESSION_SERVER_API_URL); }); test("should use provided base url if set", () => { const sessionManager = new SessionManager({ sessionServerBaseUrl: "https://example.com" }); expect(sessionManager.sessionServerBaseUrl).toBe("https://example.com"); }); test("should keep sessionNamespace undefined if not set", () => { const sessionManager = new SessionManager({}); expect(sessionManager.sessionNamespace).toBeUndefined(); }); test("should use provided session namespace if set", () => { const sessionManager = new SessionManager({ sessionNamespace: "test" }); expect(sessionManager.sessionNamespace).toBe("test"); }); test("should use default session time if not set", () => { const sessionManager = new SessionManager({}); expect(sessionManager.sessionTime).toBe(86400); }); test("should use provided session time if set", () => { const sessionManager = new SessionManager({ sessionTime: 1000 }); expect(sessionManager.sessionTime).toBe(1000); }); test("should keep allowedOrigin '*' if not set", () => { const sessionManager = new SessionManager({}); expect(sessionManager.allowedOrigin).toBe("*"); }); test("should use provided allowedOrigin if set", () => { const sessionManager = new SessionManager({ allowedOrigin: "https://example.com" }); expect(sessionManager.allowedOrigin).toBe("https://example.com"); }); }); describe("Invalid SessionId Handling", () => { describe("Empty or missing sessionId", () => { test("should treat empty string sessionId as not set (falsy check)", () => { // @ts-expect-error - testing invalid input const sessionManager = new SessionManager({ sessionId: "" }); expect(sessionManager.sessionId).toBeUndefined(); }); test("should throw error on createSession when sessionId not set", async () => { const sessionManager = new SessionManager({}); await expect(sessionManager.createSession({ testData: "test" })).rejects.toThrow("Session id is required"); }); test("should throw error on authorizeSession when sessionId not set", async () => { const sessionManager = new SessionManager({}); await expect(sessionManager.authorizeSession()).rejects.toThrow("Session id is required"); }); test("should throw error on updateSession when sessionId not set", async () => { const sessionManager = new SessionManager({}); await expect(sessionManager.updateSession({ testData: "test" })).rejects.toThrow("Session id is required"); }); test("should throw error on invalidateSession when sessionId not set", async () => { const sessionManager = new SessionManager({}); await expect(sessionManager.invalidateSession()).rejects.toThrow("Session id is required"); }); }); describe("Invalid hex format", () => { test("should throw error for non-hex characters", () => { // @ts-expect-error - testing invalid input expect(() => new SessionManager({ sessionId: "ZZZZZZ" })).toThrow("Session id must be a hex string"); }); test("should throw error for special characters", () => { // @ts-expect-error - testing invalid input expect(() => new SessionManager({ sessionId: "abc!@#$%^" })).toThrow("Session id must be a hex string"); }); test("should throw error for spaces", () => { // @ts-expect-error - testing invalid input expect(() => new SessionManager({ sessionId: "abc def" })).toThrow("Session id must be a hex string"); }); test("should throw error for only 0x prefix", () => { expect(() => new SessionManager({ sessionId: "0x" })).toThrow("Session id must be a hex string"); }); test("should throw error for invalid characters after 0x prefix", () => { expect(() => new SessionManager({ sessionId: "0xGGGGGG" })).toThrow("Session id must be a hex string"); }); test("setSessionId should throw for invalid hex", () => { const sessionManager = new SessionManager({}); expect(() => sessionManager.setSessionId("invalid-hex!!!" as Hex)).toThrow("Session id must be a hex string"); }); test("setSessionId should throw for empty string", () => { const sessionManager = new SessionManager({}); expect(() => sessionManager.setSessionId("" as Hex)).toThrow("Session id must be a hex string"); }); }); describe("Valid hex formats", () => { test("should handle short hex string (pads to 64 chars)", () => { const sessionManager = new SessionManager({ sessionId: "0xabc" }); expect(sessionManager.sessionId).toHaveLength(66); expect(sessionManager.sessionId).toMatch(/^0x[0-9a-f]{64}$/); expect(sessionManager.sessionId).toMatch(/abc$/); }); test("should handle full 64-char hex string without prefix", () => { const fullHex = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; const sessionManager = new SessionManager({ sessionId: fullHex as Hex }); expect(sessionManager.sessionId).toBe(`0x${fullHex}`); }); test("should handle full 64-char hex string with 0x prefix", () => { const fullHex = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; const sessionManager = new SessionManager({ sessionId: fullHex }); expect(sessionManager.sessionId).toBe(fullHex); }); test("should handle uppercase hex characters", () => { const sessionManager = new SessionManager({ sessionId: "0xABCDEF" }); expect(sessionManager.sessionId).toHaveLength(66); expect(sessionManager.sessionId).toContain("ABCDEF"); }); test("should handle mixed case hex characters", () => { const sessionManager = new SessionManager({ sessionId: "0xAbCdEf123456" }); expect(sessionManager.sessionId).toHaveLength(66); }); test("should handle odd-length hex string", () => { const sessionManager = new SessionManager({ sessionId: "0xabc" }); expect(sessionManager.sessionId).toHaveLength(66); }); test("should handle single hex digit", () => { const sessionManager = new SessionManager({ sessionId: "0xf" }); expect(sessionManager.sessionId).toHaveLength(66); expect(sessionManager.sessionId).toMatch(/^0x0+f$/); }); }); describe("Truncation behavior", () => { test("should truncate hex string longer than 64 characters", () => { const baseHex = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; const longHex: Hex = `0x${baseHex}aaaa1111`; const sessionManager = new SessionManager({ sessionId: longHex }); expect(sessionManager.sessionId).toHaveLength(66); expect(sessionManager.sessionId).toBe("0x" + baseHex); }); test("should truncate without 0x prefix when longer than 64 characters", () => { const baseHex = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; const longHex = baseHex + "abcd"; const sessionManager = new SessionManager({ sessionId: longHex as Hex }); expect(sessionManager.sessionId).toHaveLength(66); expect(sessionManager.sessionId).toBe("0x" + baseHex); }); }); }); describe("Session Creation Flow", () => { let sessionManager: SessionManager<{ testData: string }>; let sessionId: Hex; beforeEach(() => { sessionId = SessionManager.generateRandomSessionKey(); sessionManager = new SessionManager({ sessionId }); }); test("should create and authorize session", async () => { // Step 1: Create session await sessionManager.createSession({ testData: "test" }); // Step 2: Authorize and verify data const sessionData = await sessionManager.authorizeSession(); expect(sessionData).toEqual({ testData: "test" }); }); }); describe("Session Update Flow", () => { let sessionManager: SessionManager<{ testData: string }>; beforeEach(async () => { const sessionId = SessionManager.generateRandomSessionKey(); sessionManager = new SessionManager({ sessionId }); await sessionManager.createSession({ testData: "test" }); }); test("should update and verify session data", async () => { // Step 1: Update session await sessionManager.updateSession({ testData: "test2" }); // Step 2: Verify updated data const sessionData = await sessionManager.authorizeSession(); expect(sessionData).toEqual({ testData: "test2" }); }); }); describe("Session Invalidation Flow", () => { let sessionManager: SessionManager<{ testData: string }>; let sessionId: Hex; beforeEach(async () => { sessionId = SessionManager.generateRandomSessionKey(); sessionManager = new SessionManager({ sessionId }); await sessionManager.createSession({ testData: "test" }); }); test("should invalidate session correctly", async () => { // Step 1: Invalidate session await sessionManager.invalidateSession(); expect(sessionManager.sessionId).toBe(undefined); // Step 2: Restore session ID sessionManager.setSessionId(sessionId); // Step 3: Verify session is empty const sessionData = await sessionManager.authorizeSession(); expect(sessionData).toEqual({}); }); }); describe("Session Key Generation", () => { test("should generate valid session key", () => { const sessionKey = SessionManager.generateRandomSessionKey(); expect(sessionKey).toHaveLength(66); expect(sessionKey).toMatch(/^0x[0-9a-f]+$/); }); }); describe("Timeout Validation", () => { test("should throw error if timout expires", async () => { const sessionManager = new SessionManager({ sessionId: SessionManager.generateRandomSessionKey(), sessionTime: 1, }); await sessionManager.createSession({ testData: "test" }); await new Promise((resolve) => { setTimeout(resolve, 2000); }); await expect(sessionManager.authorizeSession()).rejects.toThrow(); }); test("should allow if time is not expired", async () => { const sessionManager = new SessionManager({ sessionId: SessionManager.generateRandomSessionKey(), sessionTime: 1000, }); await sessionManager.createSession({ testData: "test" }); await new Promise((resolve) => { setTimeout(resolve, 500); }); await expect(sessionManager.authorizeSession()).resolves.toEqual({ testData: "test" }); }); }); describe("Origin Validation", () => { let sessionManager: SessionManager<{ testData: string }>; let sessionId: Hex; beforeEach(() => { sessionId = SessionManager.generateRandomSessionKey(); // Initialize with specific allowed origin sessionManager = new SessionManager({ sessionId, allowedOrigin: "https://example.com", }); }); test("should accept request from allowed origin", async () => { // Create session with matching origin header await sessionManager.createSession({ testData: "test" }, { origin: "https://example.com" }); const sessionData = await sessionManager.authorizeSession({ headers: { origin: "https://example.com" }, }); expect(sessionData).toEqual({ testData: "test" }); }); test("should reject request from disallowed origin", async () => { // Create session await sessionManager.createSession({ testData: "test" }, { origin: "https://example.com" }); // Try to authorize from different origin await expect( sessionManager.authorizeSession({ headers: { origin: "https://malicious-site.com" }, }) ).rejects.toThrow(); }); test("should accept any origin when allowedOrigin is *", async () => { // Create new session manager with wildcard origin const wildcardManager = new SessionManager({ sessionId, allowedOrigin: "*", }); await wildcardManager.createSession({ testData: "test" }, { origin: "https://any-site.com" }); const sessionData = await wildcardManager.authorizeSession({ headers: { origin: "https://different-site.com" }, }); expect(sessionData).toEqual({ testData: "test" }); }); test("should accept empty origin when allowedOrigin is *", async () => { const wildcardManager = new SessionManager({ sessionId, allowedOrigin: "*", }); await wildcardManager.createSession({ testData: "test" }, { origin: "" }); const sessionData = await wildcardManager.authorizeSession(); expect(sessionData).toEqual({ testData: "test" }); }); test("should throw error if missing origin header", async () => { await sessionManager.createSession({ testData: "test" }, { origin: "https://example.com" }); // Try to authorize without origin header await expect( sessionManager.authorizeSession({ headers: {}, }) ).rejects.toThrow(); }); test("should return session data if allowedOrigin: TRUE, origin: empty", async () => { // Create session with origin as true const newSessionId = SessionManager.generateRandomSessionKey(); const sessionManagerWithTrueOrigin = new SessionManager({ sessionId: newSessionId, allowedOrigin: true, }); await sessionManagerWithTrueOrigin.createSession({ testData: "test" }); const sessionData = await sessionManagerWithTrueOrigin.authorizeSession(); expect(sessionData).toEqual({ testData: "test" }); }); test("should throw error if allowedOrigin: TRUE, origin: set", async () => { // Create session with origin as true const newSessionId = SessionManager.generateRandomSessionKey(); const sessionManagerWithTrueOrigin = new SessionManager({ sessionId: newSessionId, allowedOrigin: true, }); await sessionManagerWithTrueOrigin.createSession({ testData: "test" }, { origin: "https://example.com" }); await expect(sessionManagerWithTrueOrigin.authorizeSession()).rejects.toThrow(); }); test("should return session data if allowedOrigin: FALSE, origin: empty", async () => { // Create session with origin as true const newSessionId = SessionManager.generateRandomSessionKey(); const sessionManagerWithFalseOrigin = new SessionManager({ sessionId: newSessionId, allowedOrigin: false, }); await sessionManagerWithFalseOrigin.createSession({ testData: "test" }); const sessionData = await sessionManagerWithFalseOrigin.authorizeSession(); expect(sessionData).toEqual({ testData: "test" }); }); test("should return session data if allowedOrigin: FALSE, origin: set", async () => { // Create session with origin as true const newSessionId = SessionManager.generateRandomSessionKey(); const sessionManagerWithFalseOrigin = new SessionManager({ sessionId: newSessionId, allowedOrigin: false, }); await sessionManagerWithFalseOrigin.createSession({ testData: "test" }, { origin: "https://example.com" }); const sessionData = await sessionManagerWithFalseOrigin.authorizeSession(); expect(sessionData).toEqual({ testData: "test" }); }); }); describe("Data Decryption Errors", () => { test("should throw error if data decryption fails", async () => { const sessionKey = SessionManager.generateRandomSessionKey(); const sessionManager = new SessionManager({ sessionId: sessionKey }); await sessionManager.createSession({ testData: "test", error: "Something went wrong" }); await expect(sessionManager.authorizeSession()).rejects.toThrow("There was an error decrypting data."); }); }); describe("Local Storage Behavior", () => { const originalWindow = globalThis.window; class MemoryStorage implements Storage { private store = new Map(); get length() { return this.store.size; } clear(): void { this.store.forEach((_value, key) => { delete (this as Record)[key]; }); this.store.clear(); } getItem(key: string): string | null { return this.store.has(key) ? this.store.get(key)! : null; } key(index: number): string | null { return Array.from(this.store.keys())[index] ?? null; } removeItem(key: string): void { this.store.delete(key); delete (this as Record)[key]; } setItem(key: string, value: string): void { this.store.set(key, value); Object.defineProperty(this, key, { value, configurable: true, enumerable: true, writable: true, }); } } let storage: Storage; let sessionId: Hex; const sessionNamespace = "test-local"; beforeEach(() => { storage = new MemoryStorage(); // Node test environment does not provide localStorage by default. Object.defineProperty(globalThis, "window", { value: { localStorage: storage }, configurable: true, }); sessionId = SessionManager.generateRandomSessionKey(); }); afterEach(() => { vi.restoreAllMocks(); Object.defineProperty(globalThis, "window", { value: originalWindow, configurable: true, }); }); test("should write to local storage while creating session", async () => { const sessionManager = new SessionManager<{ testData: string }>({ sessionId, sessionNamespace, useLocalStorage: true, }); vi.spyOn(BaseSessionManager.prototype as unknown as { request: (...args: unknown[]) => Promise }, "request").mockResolvedValue({}); await sessionManager.createSession({ testData: "test" }); expect(storage.getItem(sessionManager.localStorageHandlerStorageKey)).toBe(JSON.stringify({ testData: "test" })); }); test("should read from local storage before calling server", async () => { const sessionManager = new SessionManager<{ testData: string }>({ sessionId, sessionNamespace, useLocalStorage: true, }); const requestSpy = vi.spyOn(BaseSessionManager.prototype as unknown as { request: (...args: unknown[]) => Promise }, "request"); storage.setItem(sessionManager.localStorageHandlerStorageKey, JSON.stringify({ testData: "test" })); const result = await sessionManager.authorizeSession(); expect(result).toEqual({ testData: "test" }); expect(requestSpy).not.toHaveBeenCalled(); }); test("should ignore corrupted local cache while updating session", async () => { const sessionManager = new SessionManager<{ testData: string; count: number }>({ sessionId, sessionNamespace, useLocalStorage: true, }); const requestSpy = vi .spyOn(BaseSessionManager.prototype as unknown as { request: (...args: unknown[]) => Promise }, "request") .mockResolvedValueOnce({}); storage.setItem(sessionManager.localStorageHandlerStorageKey, "not-json"); await expect(sessionManager.updateSession({ testData: "new" })).resolves.toBeUndefined(); expect(requestSpy).toHaveBeenCalledTimes(1); expect(storage.getItem(sessionManager.localStorageHandlerStorageKey)).toBe(JSON.stringify({ testData: "new" })); }); test("should not write local storage when createSession request fails", async () => { const sessionManager = new SessionManager<{ testData: string }>({ sessionId, sessionNamespace, useLocalStorage: true, }); vi.spyOn(BaseSessionManager.prototype as unknown as { request: (...args: unknown[]) => Promise }, "request").mockRejectedValueOnce( new Error("request failed") ); await expect(sessionManager.createSession({ testData: "test" })).rejects.toThrow("request failed"); expect(storage.getItem(sessionManager.localStorageHandlerStorageKey)).toBeNull(); }); test("should not update local storage when updateSession request fails", async () => { const sessionManager = new SessionManager<{ testData: string; count: number }>({ sessionId, sessionNamespace, useLocalStorage: true, }); storage.setItem(sessionManager.localStorageHandlerStorageKey, JSON.stringify({ testData: "existing", count: 1 })); vi.spyOn(BaseSessionManager.prototype as unknown as { request: (...args: unknown[]) => Promise }, "request").mockRejectedValueOnce( new Error("request failed") ); await expect(sessionManager.updateSession({ testData: "new" })).rejects.toThrow("request failed"); expect(storage.getItem(sessionManager.localStorageHandlerStorageKey)).toBe(JSON.stringify({ testData: "existing", count: 1 })); }); test("should clear current and orphaned local storage entries", () => { const sessionManager = new SessionManager<{ testData: string }>({ sessionId, sessionNamespace, useLocalStorage: true, }); storage.setItem(sessionManager.localStorageHandlerStorageKey, JSON.stringify({ testData: "test" })); storage.setItem(`${sessionNamespace}:orphaned`, JSON.stringify({ testData: "test2" })); storage.setItem("another-namespace:keep", JSON.stringify({ testData: "keep" })); sessionManager.clearStorage(); sessionManager.clearOrphanedData(); expect(storage.getItem(sessionManager.localStorageHandlerStorageKey)).toBeNull(); expect(storage.getItem(`${sessionNamespace}:orphaned`)).toBeNull(); expect(storage.getItem("another-namespace:keep")).toBe(JSON.stringify({ testData: "keep" })); }); }); });