Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions __tests__/components/providers/WagmiSetup.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,49 @@ jest.mock("ethers", () => ({

describe("WagmiSetup Security Tests", () => {
let mockInitializeAppKit: jest.Mock;
let mockLogErrorSecurely: jest.Mock;
let mockSetToast: jest.Mock;
let mockAdapterCreateMethod: jest.Mock;
let mockUseAppWallets: jest.Mock;
const MockAppKitAdapterManager =
require("@/components/providers/AppKitAdapterManager").AppKitAdapterManager;
const originalEthereumDescriptor = Object.getOwnPropertyDescriptor(
globalThis,
"ethereum"
);
const originalSafeEthereumProxyInstalled = (
globalThis as {
__6529_safeEthereumProxyInstalled?: boolean | undefined;
}
).__6529_safeEthereumProxyInstalled;

const restoreEthereumState = () => {
if (originalEthereumDescriptor) {
Object.defineProperty(globalThis, "ethereum", originalEthereumDescriptor);
} else {
delete (globalThis as { ethereum?: unknown }).ethereum;
}

if (originalSafeEthereumProxyInstalled === undefined) {
delete (
globalThis as {
__6529_safeEthereumProxyInstalled?: boolean | undefined;
}
).__6529_safeEthereumProxyInstalled;
return;
}

(
globalThis as {
__6529_safeEthereumProxyInstalled?: boolean | undefined;
}
).__6529_safeEthereumProxyInstalled = originalSafeEthereumProxyInstalled;
};

beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers();
restoreEthereumState();

// Add unhandled rejection handler for expected errors
globalThis.addEventListener("unhandledrejection", (event) => {
Expand All @@ -126,6 +160,7 @@ describe("WagmiSetup Security Tests", () => {

mockInitializeAppKit =
require("@/utils/appkit-initialization.utils").initializeAppKit;
mockLogErrorSecurely = require("@/utils/error-sanitizer").logErrorSecurely;
mockSetToast = jest.fn();
mockAdapterCreateMethod = jest.fn();
mockUseAppWallets = useAppWallets as jest.Mock;
Expand Down Expand Up @@ -190,6 +225,7 @@ describe("WagmiSetup Security Tests", () => {

afterEach(() => {
jest.useRealTimers();
restoreEthereumState();
// Restore console.error
(console.error as jest.Mock).mockRestore?.();
});
Expand Down Expand Up @@ -223,6 +259,89 @@ describe("WagmiSetup Security Tests", () => {
});
});

describe("Ethereum Proxy Installation", () => {
const readOnlyEthereumLogContext =
"[WagmiSetup] Skipping safe ethereum proxy install for read-only window.ethereum";

it("skips proxy installation when window.ethereum is getter-only", async () => {
const provider = {
request() {
return this;
},
};

Object.defineProperty(globalThis, "ethereum", {
configurable: true,
get: () => provider,
});

await renderAndWaitForMount();

expect(mockInitializeAppKit).toHaveBeenCalled();
expect((globalThis as { ethereum?: unknown }).ethereum).toBe(provider);
expect(mockLogErrorSecurely).toHaveBeenCalledWith(
readOnlyEthereumLogContext,
expect.any(Error)
);
expect(
(
globalThis as {
__6529_safeEthereumProxyInstalled?: boolean | undefined;
}
).__6529_safeEthereumProxyInstalled
).toBe(true);
});

it("installs the proxy when window.ethereum is writable", async () => {
const provider = {
request() {
return this;
},
};

Object.defineProperty(globalThis, "ethereum", {
configurable: true,
writable: true,
value: provider,
});

await renderAndWaitForMount();

const proxiedEthereum = (globalThis as { ethereum?: any }).ethereum;
expect(mockInitializeAppKit).toHaveBeenCalled();
expect(proxiedEthereum).toBeDefined();
expect(proxiedEthereum).not.toBe(provider);
expect(proxiedEthereum.request()).toBe(provider);
expect(mockLogErrorSecurely).not.toHaveBeenCalledWith(
readOnlyEthereumLogContext,
expect.any(Error)
);
});

it("logs the read-only ethereum skip once across mounts", async () => {
const provider = {
request() {
return this;
},
};

Object.defineProperty(globalThis, "ethereum", {
configurable: true,
get: () => provider,
});

const firstRender = await renderAndWaitForMount();
firstRender.unmount();

await renderAndWaitForMount();

const readOnlyLogs = mockLogErrorSecurely.mock.calls.filter(
([context]) => context === readOnlyEthereumLogContext
);
expect(readOnlyLogs).toHaveLength(1);
});
});

// Note: Error handling tests removed due to implementation bug
// The useEffect doesn't await initializeAppKit() causing unhandled promise rejections
// when the function throws after calling setToast. This is an implementation bug.
Expand Down
Loading
Loading