@gemini-wallet/core
Version:
Core SDK for Gemini Wallet integration with popup communication
306 lines (230 loc) • 10.3 kB
text/typescript
import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test";
// Mock dependencies
mock.module("../utils", () => ({
safeJsonStringify: mock(obj => JSON.stringify(obj)),
}));
import { safeJsonStringify } from "../utils";
import { GeminiStorage } from "./storage";
import {
STORAGE_ETH_ACCOUNTS_KEY,
STORAGE_ETH_ACTIVE_CHAIN_KEY,
STORAGE_PASSKEY_CREDENTIAL_KEY,
STORAGE_SETTINGS_KEY,
STORAGE_SMART_ACCOUNT_KEY,
STORAGE_WC_REQUESTS_KEY,
} from "./storageInterface";
describe("GeminiStorage", () => {
// Mock localStorage
const mockLocalStorage = {
_storage: {} as Record<string, string>,
clear: mock(),
getItem: mock(() => null),
removeItem: mock(),
setItem: mock(),
};
// Save original localStorage
const originalLocalStorage = global.localStorage;
beforeEach(() => {
// Reset mock call counts
mockLocalStorage.getItem.mockClear();
mockLocalStorage.setItem.mockClear();
mockLocalStorage.removeItem.mockClear();
// Reset storage state
mockLocalStorage._storage = {};
// Replace global localStorage with mock
Object.defineProperty(global, "localStorage", {
value: mockLocalStorage,
writable: true,
});
});
afterEach(() => {
// Restore original localStorage
Object.defineProperty(global, "localStorage", {
value: originalLocalStorage,
writable: true,
});
});
describe("constructor", () => {
it("should initialize with default scope and module", async () => {
const storage = new GeminiStorage();
// Test the scopedKey method indirectly
await storage.setItem("test", "value");
expect(mockLocalStorage.setItem).toHaveBeenCalledWith("@gemini.wallet.test", "value");
});
});
describe("storeObject", () => {
it("should store an object as JSON string", async () => {
const storage = new GeminiStorage();
const testObject = { foo: "bar", num: 123 };
await storage.storeObject("testKey", testObject);
expect(safeJsonStringify).toHaveBeenCalledWith(testObject);
expect(mockLocalStorage.setItem).toHaveBeenCalledWith("@gemini.wallet.testKey", JSON.stringify(testObject));
});
it("should handle complex objects with bigint values", async () => {
const storage = new GeminiStorage();
const testObject = { id: "test", value: 123n };
// Mock safeJsonStringify for this specific test
(safeJsonStringify as any).mockReturnValueOnce('{"id":"test","value":"123n"}');
await storage.storeObject("testKey", testObject);
expect(safeJsonStringify).toHaveBeenCalledWith(testObject);
expect(mockLocalStorage.setItem).toHaveBeenCalledWith("@gemini.wallet.testKey", '{"id":"test","value":"123n"}');
});
});
describe("loadObject", () => {
it("should load and parse a stored object", async () => {
const storage = new GeminiStorage();
const storedJson = '{"foo":"bar","num":123}';
mockLocalStorage.getItem.mockReturnValueOnce(storedJson);
const result = await storage.loadObject("testKey", { default: true });
expect(mockLocalStorage.getItem).toHaveBeenCalledWith("@gemini.wallet.testKey");
expect(result).toEqual({ foo: "bar", num: 123 });
});
it("should return fallback when item doesn't exist", async () => {
const storage = new GeminiStorage();
const fallback = { default: true };
mockLocalStorage.getItem.mockReturnValueOnce(null);
const result = await storage.loadObject("nonExistentKey", fallback);
expect(mockLocalStorage.getItem).toHaveBeenCalledWith("@gemini.wallet.nonExistentKey");
expect(result).toEqual(fallback);
});
it("should handle JSON parse errors and return fallback", async () => {
const storage = new GeminiStorage();
const fallback = { default: true };
// Return invalid JSON
mockLocalStorage.getItem.mockReturnValueOnce("{invalid:json}");
// Mock console.error to avoid test output pollution
const consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
const result = await storage.loadObject("invalidJsonKey", fallback);
expect(mockLocalStorage.getItem).toHaveBeenCalledWith("@gemini.wallet.invalidJsonKey");
expect(result).toEqual(fallback);
// Verify error was logged
expect(consoleErrorSpy).toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});
});
describe("setItem", () => {
it("should set item with scoped key", async () => {
const storage = new GeminiStorage();
await storage.setItem("testKey", "testValue");
expect(mockLocalStorage.setItem).toHaveBeenCalledWith("@gemini.wallet.testKey", "testValue");
});
it("should handle empty string values", async () => {
const storage = new GeminiStorage();
await storage.setItem("emptyKey", "");
expect(mockLocalStorage.setItem).toHaveBeenCalledWith("@gemini.wallet.emptyKey", "");
});
});
describe("getItem", () => {
it("should get item with scoped key", async () => {
const storage = new GeminiStorage();
mockLocalStorage.getItem.mockReturnValueOnce("testValue");
const result = await storage.getItem("testKey");
expect(mockLocalStorage.getItem).toHaveBeenCalledWith("@gemini.wallet.testKey");
expect(result).toBe("testValue");
});
it("should return null for non-existent items", async () => {
const storage = new GeminiStorage();
mockLocalStorage.getItem.mockReturnValueOnce(null);
const result = await storage.getItem("nonExistentKey");
expect(mockLocalStorage.getItem).toHaveBeenCalledWith("@gemini.wallet.nonExistentKey");
expect(result).toBeNull();
});
});
describe("removeItem", () => {
it("should remove item with scoped key", async () => {
const storage = new GeminiStorage();
await storage.removeItem("testKey");
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("@gemini.wallet.testKey");
});
});
describe("removeItems", () => {
it("should remove multiple items", async () => {
const storage = new GeminiStorage();
const keys = ["key1", "key2", "key3"];
await storage.removeItems(keys);
expect(mockLocalStorage.removeItem).toHaveBeenCalledTimes(3);
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("@gemini.wallet.key1");
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("@gemini.wallet.key2");
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("@gemini.wallet.key3");
});
it("should handle empty array", async () => {
const storage = new GeminiStorage();
await storage.removeItems([]);
expect(mockLocalStorage.removeItem).not.toHaveBeenCalled();
});
it("should handle single item array", async () => {
const storage = new GeminiStorage();
await storage.removeItems(["singleKey"]);
expect(mockLocalStorage.removeItem).toHaveBeenCalledTimes(1);
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("@gemini.wallet.singleKey");
});
});
describe("storage constants", () => {
it("should export the correct storage key constants", () => {
expect(STORAGE_ETH_ACCOUNTS_KEY).toBe("eth-accounts");
expect(STORAGE_ETH_ACTIVE_CHAIN_KEY).toBe("eth-active-chain");
expect(STORAGE_PASSKEY_CREDENTIAL_KEY).toBe("passkey-credential");
expect(STORAGE_SMART_ACCOUNT_KEY).toBe("smart-account");
expect(STORAGE_SETTINGS_KEY).toBe("settings");
expect(STORAGE_WC_REQUESTS_KEY).toBe("wc-requests");
});
});
describe("integration with storage keys", () => {
it("should use correct scoped keys for predefined constants", async () => {
const storage = new GeminiStorage();
await storage.setItem(STORAGE_ETH_ACCOUNTS_KEY, "accounts-data");
expect(mockLocalStorage.setItem).toHaveBeenCalledWith("@gemini.wallet.eth-accounts", "accounts-data");
await storage.getItem(STORAGE_PASSKEY_CREDENTIAL_KEY);
expect(mockLocalStorage.getItem).toHaveBeenCalledWith("@gemini.wallet.passkey-credential");
await storage.removeItem(STORAGE_SETTINGS_KEY);
expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("@gemini.wallet.settings");
});
});
describe("memory storage fallback", () => {
let consoleWarnSpy: any;
beforeEach(() => {
// Mock console.warn to avoid test output pollution
consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {});
});
afterEach(() => {
consoleWarnSpy.mockRestore();
});
it("should fallback to memory storage when localStorage throws error", async () => {
const storage = new GeminiStorage();
// Make localStorage methods throw errors
mockLocalStorage.setItem.mockImplementation(() => {
throw new Error("localStorage not available");
});
mockLocalStorage.getItem.mockImplementation(() => {
throw new Error("localStorage not available");
});
mockLocalStorage.removeItem.mockImplementation(() => {
throw new Error("localStorage not available");
});
// Test setItem fallback
await storage.setItem("testKey", "testValue");
expect(consoleWarnSpy).toHaveBeenCalledWith(
"localStorage not available, using memory storage",
expect.any(Error),
);
// Test getItem fallback - should return value from memory storage
const value = await storage.getItem("testKey");
expect(value).toBe("testValue");
// Test removeItem fallback
await storage.removeItem("testKey");
const removedValue = await storage.getItem("testKey");
expect(removedValue).toBeNull();
});
it("should use memory storage for removeItems when localStorage throws", async () => {
const storage = new GeminiStorage();
// Make localStorage.removeItem throw error
mockLocalStorage.removeItem.mockImplementation(() => {
throw new Error("localStorage not available");
});
const keys = ["key1", "key2"];
await storage.removeItems(keys);
expect(consoleWarnSpy).toHaveBeenCalledTimes(2); // Once for each key
expect(mockLocalStorage.removeItem).toHaveBeenCalledTimes(2);
});
});
});