import { Hex } from "@toruslabs/metadata-helpers"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { BaseStorageManager } from "../src/base"; import { SESSION_SERVER_API_URL } from "../src/interfaces"; import { StorageManager } from "../src/storageManager"; const baseStorageRequestTarget = BaseStorageManager.prototype as unknown as { request: (...args: unknown[]) => Promise; }; const requiredStorageManagerOptions = { sessionServerBaseUrl: SESSION_SERVER_API_URL, } as const; describe("StorageManager", () => { describe("Input Validation", () => { test("should throw error if sessionId is not set", async () => { const storageManager = new StorageManager(requiredStorageManagerOptions); await expect(storageManager.createSession({})).rejects.toThrow("Session id is required"); }); test("should use shared session server constant", () => { const storageManager = new StorageManager(requiredStorageManagerOptions); expect(storageManager.sessionServerBaseUrl).toBe(SESSION_SERVER_API_URL); }); test("should use provided base url if set", () => { const storageManager = new StorageManager({ sessionServerBaseUrl: "https://example.com" }); expect(storageManager.sessionServerBaseUrl).toBe("https://example.com"); }); test("should keep sessionNamespace undefined if not set", () => { const storageManager = new StorageManager(requiredStorageManagerOptions); expect(storageManager.sessionNamespace).toBeUndefined(); }); test("should use provided session namespace if set", () => { const storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionNamespace: "test" }); expect(storageManager.sessionNamespace).toBe("test"); }); test("should use default session time if not set", () => { const storageManager = new StorageManager(requiredStorageManagerOptions); expect(storageManager.sessionTime).toBe(86400); }); test("should use provided session time if set", () => { const storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionTime: 1000 }); expect(storageManager.sessionTime).toBe(1000); }); test("should keep allowedOrigin '*' if not set", () => { const storageManager = new StorageManager(requiredStorageManagerOptions); expect(storageManager.allowedOrigin).toBe("*"); }); test("should use provided allowedOrigin if set", () => { const storageManager = new StorageManager({ ...requiredStorageManagerOptions, allowedOrigin: "https://example.com" }); expect(storageManager.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 storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId: "" }); expect(storageManager.sessionId).toBeUndefined(); }); test("should throw error on createSession when sessionId not set", async () => { const storageManager = new StorageManager(requiredStorageManagerOptions); await expect(storageManager.createSession({ testData: "test" })).rejects.toThrow("Session id is required"); }); test("should throw error on authorizeSession when sessionId not set", async () => { const storageManager = new StorageManager(requiredStorageManagerOptions); await expect(storageManager.authorizeSession()).rejects.toThrow("Session id is required"); }); test("should throw error on updateSession when sessionId not set", async () => { const storageManager = new StorageManager(requiredStorageManagerOptions); await expect(storageManager.updateSession({ testData: "test" })).rejects.toThrow("Session id is required"); }); test("should throw error on invalidateSession when sessionId not set", async () => { const storageManager = new StorageManager(requiredStorageManagerOptions); await expect(storageManager.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 StorageManager({ ...requiredStorageManagerOptions, 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 StorageManager({ ...requiredStorageManagerOptions, sessionId: "abc!@#$%^" })).toThrow("Session id must be a hex string"); }); test("should throw error for spaces", () => { // @ts-expect-error - testing invalid input expect(() => new StorageManager({ ...requiredStorageManagerOptions, sessionId: "abc def" })).toThrow("Session id must be a hex string"); }); test("should throw error for only 0x prefix", () => { expect(() => new StorageManager({ ...requiredStorageManagerOptions, sessionId: "0x" })).toThrow("Session id must be a hex string"); }); test("should throw error for invalid characters after 0x prefix", () => { expect(() => new StorageManager({ ...requiredStorageManagerOptions, sessionId: "0xGGGGGG" })).toThrow("Session id must be a hex string"); }); test("setSessionId should throw for invalid hex", () => { const storageManager = new StorageManager(requiredStorageManagerOptions); expect(() => storageManager.setSessionId("invalid-hex!!!" as Hex)).toThrow("Session id must be a hex string"); }); test("setSessionId should throw for empty string", () => { const storageManager = new StorageManager(requiredStorageManagerOptions); expect(() => storageManager.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 storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId: "0xabc" }); expect(storageManager.sessionId).toHaveLength(66); expect(storageManager.sessionId).toMatch(/^0x[0-9a-f]{64}$/); expect(storageManager.sessionId).toMatch(/abc$/); }); test("should handle full 64-char hex string without prefix", () => { const fullHex = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; const storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId: fullHex as Hex }); expect(storageManager.sessionId).toBe(`0x${fullHex}`); }); test("should handle full 64-char hex string with 0x prefix", () => { const fullHex = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; const storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId: fullHex }); expect(storageManager.sessionId).toBe(fullHex); }); test("should handle uppercase hex characters", () => { const storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId: "0xABCDEF" }); expect(storageManager.sessionId).toHaveLength(66); expect(storageManager.sessionId).toContain("ABCDEF"); }); test("should handle mixed case hex characters", () => { const storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId: "0xAbCdEf123456" }); expect(storageManager.sessionId).toHaveLength(66); }); test("should handle odd-length hex string", () => { const storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId: "0xabc" }); expect(storageManager.sessionId).toHaveLength(66); }); test("should handle single hex digit", () => { const storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId: "0xf" }); expect(storageManager.sessionId).toHaveLength(66); expect(storageManager.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 storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId: longHex }); expect(storageManager.sessionId).toHaveLength(66); expect(storageManager.sessionId).toBe("0x" + baseHex); }); test("should truncate without 0x prefix when longer than 64 characters", () => { const baseHex = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; const longHex = baseHex + "abcd"; const storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId: longHex as Hex }); expect(storageManager.sessionId).toHaveLength(66); expect(storageManager.sessionId).toBe("0x" + baseHex); }); }); }); describe("Session Creation Flow", () => { let storageManager: StorageManager<{ testData: string }>; let sessionId: Hex; beforeEach(() => { sessionId = StorageManager.generateRandomSessionKey(); storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId }); }); test("should create and authorize session", async () => { // Step 1: Create session await storageManager.createSession({ testData: "test" }); // Step 2: Authorize and verify data const sessionData = await storageManager.authorizeSession(); expect(sessionData).toEqual({ testData: "test" }); }); }); describe("Session Update Flow", () => { let storageManager: StorageManager<{ testData: string }>; beforeEach(async () => { const sessionId = StorageManager.generateRandomSessionKey(); storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId }); await storageManager.createSession({ testData: "test" }); }); test("should update and verify session data", async () => { // Step 1: Update session await storageManager.updateSession({ testData: "test2" }); // Step 2: Verify updated data const sessionData = await storageManager.authorizeSession(); expect(sessionData).toEqual({ testData: "test2" }); }); }); describe("Session Invalidation Flow", () => { let storageManager: StorageManager<{ testData: string }>; let sessionId: Hex; beforeEach(async () => { sessionId = StorageManager.generateRandomSessionKey(); storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId }); await storageManager.createSession({ testData: "test" }); }); test("should invalidate session correctly", async () => { // Step 1: Invalidate session await storageManager.invalidateSession(); expect(storageManager.sessionId).toBe(undefined); // Step 2: Restore session ID storageManager.setSessionId(sessionId); // Step 3: Verify session is empty const sessionData = await storageManager.authorizeSession(); expect(sessionData).toEqual({}); }); }); describe("Session Key Generation", () => { test("should generate valid session key", () => { const sessionKey = StorageManager.generateRandomSessionKey(); expect(sessionKey).toHaveLength(66); expect(sessionKey).toMatch(/^0x[0-9a-f]+$/); }); }); describe("Timeout Validation", () => { test("should throw error if timout expires", async () => { const storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId: StorageManager.generateRandomSessionKey(), sessionTime: 1, }); await storageManager.createSession({ testData: "test" }); await new Promise((resolve) => { setTimeout(resolve, 2000); }); await expect(storageManager.authorizeSession()).rejects.toThrow(); }); test("should allow if time is not expired", async () => { const storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId: StorageManager.generateRandomSessionKey(), sessionTime: 1000, }); await storageManager.createSession({ testData: "test" }); await new Promise((resolve) => { setTimeout(resolve, 500); }); await expect(storageManager.authorizeSession()).resolves.toEqual({ testData: "test" }); }); }); describe("Origin Validation", () => { let storageManager: StorageManager<{ testData: string }>; let sessionId: Hex; beforeEach(() => { sessionId = StorageManager.generateRandomSessionKey(); // Initialize with specific allowed origin storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId, allowedOrigin: "https://example.com", }); }); test("should accept request from allowed origin", async () => { // Create session with matching origin header await storageManager.createSession({ testData: "test" }, { origin: "https://example.com" }); const sessionData = await storageManager.authorizeSession({ headers: { origin: "https://example.com" }, }); expect(sessionData).toEqual({ testData: "test" }); }); test("should reject request from disallowed origin", async () => { // Create session await storageManager.createSession({ testData: "test" }, { origin: "https://example.com" }); // Try to authorize from different origin await expect( storageManager.authorizeSession({ headers: { origin: "https://malicious-site.com" }, }) ).rejects.toThrow(); }); test("should accept any origin when allowedOrigin is *", async () => { // Create new storage manager with wildcard origin const wildcardManager = new StorageManager({ ...requiredStorageManagerOptions, 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 StorageManager({ ...requiredStorageManagerOptions, 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 storageManager.createSession({ testData: "test" }, { origin: "https://example.com" }); // Try to authorize without origin header await expect( storageManager.authorizeSession({ headers: {}, }) ).rejects.toThrow(); }); test("should return session data if allowedOrigin: TRUE, origin: empty", async () => { // Create session with origin as true const newSessionId = StorageManager.generateRandomSessionKey(); const storageManagerWithTrueOrigin = new StorageManager({ ...requiredStorageManagerOptions, sessionId: newSessionId, allowedOrigin: true, }); await storageManagerWithTrueOrigin.createSession({ testData: "test" }); const sessionData = await storageManagerWithTrueOrigin.authorizeSession(); expect(sessionData).toEqual({ testData: "test" }); }); test("should throw error if allowedOrigin: TRUE, origin: set", async () => { // Create session with origin as true const newSessionId = StorageManager.generateRandomSessionKey(); const storageManagerWithTrueOrigin = new StorageManager({ ...requiredStorageManagerOptions, sessionId: newSessionId, allowedOrigin: true, }); await storageManagerWithTrueOrigin.createSession({ testData: "test" }, { origin: "https://example.com" }); await expect(storageManagerWithTrueOrigin.authorizeSession()).rejects.toThrow(); }); test("should return session data if allowedOrigin: FALSE, origin: empty", async () => { // Create session with origin as true const newSessionId = StorageManager.generateRandomSessionKey(); const storageManagerWithFalseOrigin = new StorageManager({ ...requiredStorageManagerOptions, sessionId: newSessionId, allowedOrigin: false, }); await storageManagerWithFalseOrigin.createSession({ testData: "test" }); const sessionData = await storageManagerWithFalseOrigin.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 = StorageManager.generateRandomSessionKey(); const storageManagerWithFalseOrigin = new StorageManager({ ...requiredStorageManagerOptions, sessionId: newSessionId, allowedOrigin: false, }); await storageManagerWithFalseOrigin.createSession({ testData: "test" }, { origin: "https://example.com" }); const sessionData = await storageManagerWithFalseOrigin.authorizeSession(); expect(sessionData).toEqual({ testData: "test" }); }); }); describe("Data Decryption Errors", () => { test("should throw error if data decryption fails", async () => { const sessionKey = StorageManager.generateRandomSessionKey(); const storageManager = new StorageManager({ ...requiredStorageManagerOptions, sessionId: sessionKey }); await storageManager.createSession({ testData: "test", error: "Something went wrong" }); await expect(storageManager.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 = StorageManager.generateRandomSessionKey(); }); afterEach(() => { vi.restoreAllMocks(); Object.defineProperty(globalThis, "window", { value: originalWindow, configurable: true, }); }); test("should write to local storage while creating session", async () => { const storageManager = new StorageManager<{ testData: string }>({ ...requiredStorageManagerOptions, sessionId, sessionNamespace, useLocalStorage: true, }); vi.spyOn(baseStorageRequestTarget, "request").mockResolvedValue({}); await storageManager.createSession({ testData: "test" }); expect(storage.getItem(storageManager.localStorageHandlerStorageKey)).toBe(JSON.stringify({ testData: "test" })); }); test("should read from local storage before calling server", async () => { const storageManager = new StorageManager<{ testData: string }>({ ...requiredStorageManagerOptions, sessionId, sessionNamespace, useLocalStorage: true, }); const requestSpy = vi.spyOn(baseStorageRequestTarget, "request"); storage.setItem(storageManager.localStorageHandlerStorageKey, JSON.stringify({ testData: "test" })); const result = await storageManager.authorizeSession(); expect(result).toEqual({ testData: "test" }); expect(requestSpy).not.toHaveBeenCalled(); }); test("should ignore corrupted local cache while updating session", async () => { const storageManager = new StorageManager<{ testData: string; count: number }>({ ...requiredStorageManagerOptions, sessionId, sessionNamespace, useLocalStorage: true, }); const requestSpy = vi.spyOn(baseStorageRequestTarget, "request").mockResolvedValueOnce({}); storage.setItem(storageManager.localStorageHandlerStorageKey, "not-json"); await expect(storageManager.updateSession({ testData: "new" })).resolves.toBeUndefined(); expect(requestSpy).toHaveBeenCalledTimes(1); expect(storage.getItem(storageManager.localStorageHandlerStorageKey)).toBe(JSON.stringify({ testData: "new" })); }); test("should not write local storage when createSession request fails", async () => { const storageManager = new StorageManager<{ testData: string }>({ ...requiredStorageManagerOptions, sessionId, sessionNamespace, useLocalStorage: true, }); vi.spyOn(baseStorageRequestTarget, "request").mockRejectedValueOnce(new Error("request failed")); await expect(storageManager.createSession({ testData: "test" })).rejects.toThrow("request failed"); expect(storage.getItem(storageManager.localStorageHandlerStorageKey)).toBeNull(); }); test("should not update local storage when updateSession request fails", async () => { const storageManager = new StorageManager<{ testData: string; count: number }>({ ...requiredStorageManagerOptions, sessionId, sessionNamespace, useLocalStorage: true, }); storage.setItem(storageManager.localStorageHandlerStorageKey, JSON.stringify({ testData: "existing", count: 1 })); vi.spyOn(baseStorageRequestTarget, "request").mockRejectedValueOnce(new Error("request failed")); await expect(storageManager.updateSession({ testData: "new" })).rejects.toThrow("request failed"); expect(storage.getItem(storageManager.localStorageHandlerStorageKey)).toBe(JSON.stringify({ testData: "existing", count: 1 })); }); test("should clear current and orphaned local storage entries", () => { const storageManager = new StorageManager<{ testData: string }>({ ...requiredStorageManagerOptions, sessionId, sessionNamespace, useLocalStorage: true, }); storage.setItem(storageManager.localStorageHandlerStorageKey, JSON.stringify({ testData: "test" })); storage.setItem(`${sessionNamespace}:orphaned`, JSON.stringify({ testData: "test2" })); storage.setItem("another-namespace:keep", JSON.stringify({ testData: "keep" })); storageManager.clearStorage(); storageManager.clearOrphanedData(); expect(storage.getItem(storageManager.localStorageHandlerStorageKey)).toBeNull(); expect(storage.getItem(`${sessionNamespace}:orphaned`)).toBeNull(); expect(storage.getItem("another-namespace:keep")).toBe(JSON.stringify({ testData: "keep" })); }); }); });