From c0d7b4d3a629ebac15b9b47336c42d95afad9742 Mon Sep 17 00:00:00 2001 From: Henry Eulam Eliazar <286891514+eulami@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:04:19 +0000 Subject: [PATCH] test: add unit tests for env validation, middleware CSP, and wallet connectors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive test coverage for three untested modules assigned to eulami (issues #935, #936, #937): - src/config/env/__tests__/schema.test.ts: Tests for validateEnv and validateEnvRequirements covering defaults, type coercion, enum validation, and environment-specific requirements (dev/staging/prod). - src/__tests__/middleware.test.ts: Tests for CSP header generation, nonce uniqueness, dev-vs-prod directive differences, API path skipping, and x-nonce request header injection. - src/lib/walletConnectors/__tests__/walletConnectors.test.ts: Tests for MetaMask, Coinbase, and WalletConnect adapter mappings covering success paths, missing wallet, wrong provider, user rejection, pending request errors, and availability checks. Also renames jest.config.js → jest.config.cjs to fix ESM/CJS conflict with "type": "module" in package.json, and removes the global @walletconnect/web3-provider mock from jest.setup.js (now per-test). Closes #935, closes #936, closes #937 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- jest.config.js => jest.config.cjs | 0 jest.setup.js | 15 +- src/__tests__/middleware.test.ts | 218 ++++++++++++ src/config/env/__tests__/schema.test.ts | 135 ++++++++ .../__tests__/walletConnectors.test.ts | 310 ++++++++++++++++++ 5 files changed, 666 insertions(+), 12 deletions(-) rename jest.config.js => jest.config.cjs (100%) create mode 100644 src/__tests__/middleware.test.ts create mode 100644 src/config/env/__tests__/schema.test.ts create mode 100644 src/lib/walletConnectors/__tests__/walletConnectors.test.ts diff --git a/jest.config.js b/jest.config.cjs similarity index 100% rename from jest.config.js rename to jest.config.cjs diff --git a/jest.setup.js b/jest.setup.js index 53202842..fa8d12de 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -65,24 +65,15 @@ Object.defineProperty(window, 'ethereum', { writable: true, }) -// Mock Web3Wallet -jest.mock('@walletconnect/web3-provider', () => { - return jest.fn().mockImplementation(() => ({ - enable: jest.fn(), - on: jest.fn(), - close: jest.fn(), - })) -}) - -// Mock Coinbase Wallet SDK +// Web3Wallet, Coinbase Wallet SDK, and MetaMask SDK mocks +// are defined per-test in walletConnectors tests to allow dynamic behavior. +// Mocks for other suites that need generic stubs: jest.mock('@coinbase/wallet-sdk', () => { return jest.fn().mockImplementation(() => ({ makeWeb3Provider: jest.fn(), disconnect: jest.fn(), })) }) - -// Mock MetaMask SDK jest.mock('@metamask/sdk', () => { return jest.fn().mockImplementation(() => ({ connect: jest.fn(), diff --git a/src/__tests__/middleware.test.ts b/src/__tests__/middleware.test.ts new file mode 100644 index 00000000..16f9ce09 --- /dev/null +++ b/src/__tests__/middleware.test.ts @@ -0,0 +1,218 @@ +/** + * Tests for src/middleware.ts + * Covers CSP header generation, nonce uniqueness, and API-path skip behaviour. + * Issue #936 + */ + +import type { NextRequest } from 'next/server'; + +// Track all headers set on responses across module resets +let capturedHeaders: Map; +// Track calls to NextResponse.next across module resets +let nextCalls: unknown[][]; + +jest.mock('next/server', () => { + capturedHeaders = new Map(); + nextCalls = []; + return { + NextResponse: { + next: jest.fn((...args: unknown[]) => { + nextCalls.push(args); + return { + headers: { + set: jest.fn((key: string, value: string) => { + capturedHeaders.set(key, value); + }), + get: jest.fn((key: string) => capturedHeaders.get(key)), + }, + }; + }), + }, + }; +}); + +jest.mock('@/lib/initRedisCache', () => ({ + initRedisCacheSystem: jest.fn().mockResolvedValue(undefined), +})); + +jest.mock('@/utils/logger', () => ({ + logger: { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + debug: jest.fn(), + }, +})); + +// Helper to create a mock NextRequest with proper headers +function createMockRequest( + pathname: string, + acceptHeader = 'text/html', +): NextRequest { + const headersObj: Record = {}; + if (acceptHeader) { + headersObj['accept'] = acceptHeader; + } + + return { + nextUrl: { pathname }, + headers: { + get: (name: string) => headersObj[name] ?? null, + forEach: (cb: (value: string, key: string) => void) => { + Object.entries(headersObj).forEach(([k, v]) => cb(v, k)); + }, + entries: () => Object.entries(headersObj)[Symbol.iterator](), + [Symbol.iterator]: () => Object.entries(headersObj)[Symbol.iterator](), + }, + } as unknown as NextRequest; +} + +// Reset state before each test +function resetState() { + capturedHeaders = new Map(); + nextCalls = []; +} + +describe('middleware CSP enforcement', () => { + const originalEnv = process.env; + + beforeEach(() => { + resetState(); + process.env = { ...originalEnv }; + process.env.NODE_ENV = 'production'; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + it('returns NextResponse.next() when CSP_ENFORCE is not true', async () => { + process.env.CSP_ENFORCE = 'false'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/')); + + expect(nextCalls.length).toBeGreaterThan(0); + expect(capturedHeaders.has('Content-Security-Policy')).toBe(false); + }); + + it('adds CSP header and nonce for HTML requests when CSP_ENFORCE=true', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'production'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/')); + + expect(capturedHeaders.has('Content-Security-Policy')).toBe(true); + expect(capturedHeaders.get('Content-Security-Policy')).toContain( + "default-src 'self'", + ); + }); + + it('skips CSP for API routes', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'production'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/api/csp-report')); + + expect(capturedHeaders.has('Content-Security-Policy')).toBe(false); + expect(nextCalls.length).toBeGreaterThan(0); + }); + + it('skips CSP for non-HTML accept headers', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'production'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/page', 'application/json')); + + expect(capturedHeaders.has('Content-Security-Policy')).toBe(false); + expect(nextCalls.length).toBeGreaterThan(0); + }); + + it('generates unique nonces for different requests', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'production'; + + const nonces = new Set(); + + for (let i = 0; i < 20; i++) { + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/')); + + const cspHeader = capturedHeaders.get('Content-Security-Policy'); + if (cspHeader) { + const nonceMatch = cspHeader.match(/nonce-([A-Za-z0-9+/=]+)/); + if (nonceMatch) nonces.add(nonceMatch[1]); + } + } + + expect(nonces.size).toBeGreaterThan(1); + }); + + it('includes upgrade-insecure-requests in production', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'production'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/')); + + const cspHeader = capturedHeaders.get('Content-Security-Policy'); + expect(cspHeader).toContain('upgrade-insecure-requests'); + }); + + it('does not include unsafe-eval in production CSP', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'production'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/')); + + const cspHeader = capturedHeaders.get('Content-Security-Policy'); + expect(cspHeader).not.toContain("'unsafe-eval'"); + }); + + it('includes unsafe-eval in development CSP', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'development'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/')); + + const cspHeader = capturedHeaders.get('Content-Security-Policy'); + expect(cspHeader).toContain("'unsafe-eval'"); + }); + + it('sets x-nonce header on the request', async () => { + process.env.CSP_ENFORCE = 'true'; + process.env.NODE_ENV = 'production'; + jest.resetModules(); + resetState(); + + const { middleware } = await import('../middleware'); + await middleware(createMockRequest('/')); + + // The x-nonce should be set on the request headers (passed to NextResponse.next) + expect(nextCalls.length).toBeGreaterThan(0); + const callArgs = nextCalls[0]; + expect(callArgs[0]).toHaveProperty('request'); + }); +}); diff --git a/src/config/env/__tests__/schema.test.ts b/src/config/env/__tests__/schema.test.ts new file mode 100644 index 00000000..78c950be --- /dev/null +++ b/src/config/env/__tests__/schema.test.ts @@ -0,0 +1,135 @@ +/** + * Tests for src/config/env/schema.ts + * Covers validateEnv and validateEnvRequirements boot-time env validation. + * Issue #935 + */ + +import { validateEnv, validateEnvRequirements, envSchema } from '../schema'; + +describe('envSchema', () => { + it('applies defaults for optional fields', () => { + const result = envSchema.safeParse({}); + expect(result.success).toBe(true); + if (!result.success) return; + + expect(result.data.NEXT_PUBLIC_APP_NAME).toBe('PropChain'); + expect(result.data.NODE_ENV).toBe('development'); + expect(result.data.CSP_ENFORCE).toBe(false); + expect(result.data.NEXT_PUBLIC_DEFAULT_LOCALE).toBe('en'); + expect(result.data.NEXT_PUBLIC_SUPPORTED_LOCALES).toBe('en,es,fr,de,zh,ar,he'); + }); + + it('transforms string booleans correctly', () => { + const result = envSchema.safeParse({ + CSP_ENFORCE: 'true', + NEXT_PUBLIC_ANALYTICS_ENABLED: 'true', + NEXT_PUBLIC_DEBUG_MODE: 'true', + }); + expect(result.success).toBe(true); + if (!result.success) return; + + expect(result.data.CSP_ENFORCE).toBe(true); + expect(result.data.NEXT_PUBLIC_ANALYTICS_ENABLED).toBe(true); + expect(result.data.NEXT_PUBLIC_DEBUG_MODE).toBe(true); + }); + + it('defaults string booleans to false when unset', () => { + const result = envSchema.safeParse({}); + expect(result.success).toBe(true); + if (!result.success) return; + + expect(result.data.NEXT_PUBLIC_ANALYTICS_ENABLED).toBe(false); + expect(result.data.NEXT_PUBLIC_ERROR_REPORTING_ENABLED).toBe(true); + expect(result.data.NEXT_PUBLIC_DEBUG_MODE).toBe(false); + expect(result.data.NEXT_PUBLIC_MAINTENANCE_MODE).toBe(false); + expect(result.data.NEXT_PUBLIC_USE_MOCK_DATA).toBe(false); + expect(result.data.NEXT_PUBLIC_SKIP_AUTH).toBe(false); + }); + + it('parses rate limit numeric strings', () => { + const result = envSchema.safeParse({ + RATE_LIMIT_WINDOW_MS: '60000', + RATE_LIMIT_MAX_REQUESTS: '200', + RATE_LIMIT_MAX_REQUESTS_PER_WALLET: '10', + }); + expect(result.success).toBe(true); + if (!result.success) return; + + expect(result.data.RATE_LIMIT_WINDOW_MS).toBe(60000); + expect(result.data.RATE_LIMIT_MAX_REQUESTS).toBe(200); + expect(result.data.RATE_LIMIT_MAX_REQUESTS_PER_WALLET).toBe(10); + }); +}); + +describe('validateEnv', () => { + const originalEnv = process.env; + + beforeEach(() => { + process.env = { ...originalEnv }; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + it('returns validated config when env has valid values', () => { + process.env.NEXT_PUBLIC_APP_NAME = 'TestApp'; + process.env.NODE_ENV = 'development'; + + const config = validateEnv(); + expect(config.NEXT_PUBLIC_APP_NAME).toBe('TestApp'); + expect(config.NODE_ENV).toBe('development'); + }); + + it('throws when NODE_ENV is not a valid enum value', () => { + process.env.NODE_ENV = 'invalid-env'; + + expect(() => validateEnv()).toThrow('Environment validation failed'); + }); +}); + +describe('validateEnvRequirements', () => { + it('does not throw for development without RPC URLs', () => { + const config = { + NODE_ENV: 'development' as const, + ETHEREUM_MAINNET_RPC_URL: undefined, + NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: undefined, + } as ReturnType; + + expect(() => validateEnvRequirements(config)).not.toThrow(); + }); + + it('throws for staging when required RPC URL is missing', () => { + const config = { + NODE_ENV: 'staging' as const, + ETHEREUM_MAINNET_RPC_URL: undefined, + NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: undefined, + } as ReturnType; + + expect(() => validateEnvRequirements(config)).toThrow( + /Environment-specific requirements for 'staging' are not met/, + ); + }); + + it('does not throw for staging with all required fields', () => { + const config = { + NODE_ENV: 'staging' as const, + ETHEREUM_MAINNET_RPC_URL: 'https://eth.example.com', + NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: 'test-project-id', + } as ReturnType; + + expect(() => validateEnvRequirements(config)).not.toThrow(); + }); + + it('warns (does not throw) for production with missing optional fields', () => { + const config = { + NODE_ENV: 'production' as const, + ETHEREUM_MAINNET_RPC_URL: undefined, + POLYGON_MAINNET_RPC_URL: undefined, + BSC_MAINNET_RPC_URL: undefined, + NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: undefined, + } as ReturnType; + + expect(() => validateEnvRequirements(config)).not.toThrow(); + }); +}); diff --git a/src/lib/walletConnectors/__tests__/walletConnectors.test.ts b/src/lib/walletConnectors/__tests__/walletConnectors.test.ts new file mode 100644 index 00000000..d8bcff89 --- /dev/null +++ b/src/lib/walletConnectors/__tests__/walletConnectors.test.ts @@ -0,0 +1,310 @@ +/** + * Tests for src/lib/walletConnectors/ + * Covers MetaMask, Coinbase, and WalletConnect adapter mappings. + * Issue #937 + */ + +// Mock window.ethereum for MetaMask and Coinbase tests +const mockEthereum = { + request: jest.fn(), + on: jest.fn(), + removeListener: jest.fn(), + isMetaMask: true, + isCoinbaseWallet: true, +}; + +Object.defineProperty(window, 'ethereum', { + value: mockEthereum, + writable: true, +}); + +// ─── MetaMask ──────────────────────────────────────────────────────────────── + +describe('MetaMask connector', () => { + beforeEach(() => { + jest.resetModules(); + mockEthereum.request.mockReset(); + mockEthereum.isMetaMask = true; + }); + + async function loadMetaMask() { + const mod = await import('../metamask'); + return mod; + } + + it('connectMetaMaskWallet returns address and chainId on success', async () => { + mockEthereum.request + .mockResolvedValueOnce(['0xABC123']) // eth_requestAccounts + .mockResolvedValueOnce('0x1'); // eth_chainId + + const { connectMetaMaskWallet } = await loadMetaMask(); + const result = await connectMetaMaskWallet(); + + expect(result).toEqual({ + address: '0xABC123', + chainId: 1, + }); + }); + + it('throws when window.ethereum is not present', async () => { + const original = window.ethereum; + // @ts-expect-error testing missing ethereum + window.ethereum = undefined; + + const { connectMetaMaskWallet } = await loadMetaMask(); + await expect(connectMetaMaskWallet()).rejects.toThrow( + 'MetaMask is not installed', + ); + + window.ethereum = original; + }); + + it('throws when MetaMask is not the active provider', async () => { + mockEthereum.isMetaMask = false; + + const { connectMetaMaskWallet } = await loadMetaMask(); + await expect(connectMetaMaskWallet()).rejects.toThrow( + 'MetaMask extension not detected', + ); + }); + + it('throws when no accounts are returned', async () => { + mockEthereum.request.mockResolvedValueOnce([]); + + const { connectMetaMaskWallet } = await loadMetaMask(); + await expect(connectMetaMaskWallet()).rejects.toThrow( + 'No accounts returned from MetaMask', + ); + }); + + it('handles user rejection (error code 4001)', async () => { + mockEthereum.request.mockRejectedValueOnce({ code: 4001 }); + + const { connectMetaMaskWallet } = await loadMetaMask(); + await expect(connectMetaMaskWallet()).rejects.toThrow( + 'You rejected the connection request', + ); + }); + + it('handles pending request error (code -32002)', async () => { + mockEthereum.request.mockRejectedValueOnce({ code: -32002 }); + + const { connectMetaMaskWallet } = await loadMetaMask(); + await expect(connectMetaMaskWallet()).rejects.toThrow( + 'MetaMask connection request is already pending', + ); + }); + + it('isMetaMaskAvailable returns false when window.ethereum is missing', async () => { + const original = window.ethereum; + // @ts-expect-error testing missing ethereum + window.ethereum = undefined; + + const { isMetaMaskAvailable } = await loadMetaMask(); + expect(isMetaMaskAvailable()).toBe(false); + + window.ethereum = original; + }); + + it('isMetaMaskAvailable returns true when MetaMask is present', async () => { + mockEthereum.isMetaMask = true; + + const { isMetaMaskAvailable } = await loadMetaMask(); + expect(isMetaMaskAvailable()).toBe(true); + }); +}); + +// ─── Coinbase ──────────────────────────────────────────────────────────────── + +describe('Coinbase connector', () => { + beforeEach(() => { + jest.resetModules(); + mockEthereum.request.mockReset(); + mockEthereum.isCoinbaseWallet = true; + mockEthereum.isMetaMask = false; + }); + + async function loadCoinbase() { + const mod = await import('../coinbase'); + return mod; + } + + it('connectCoinbaseWallet returns address and chainId on success', async () => { + mockEthereum.request + .mockResolvedValueOnce(['0xDEF456']) // eth_requestAccounts + .mockResolvedValueOnce('0x89'); // eth_chainId (137 = Polygon) + + const { connectCoinbaseWallet } = await loadCoinbase(); + const result = await connectCoinbaseWallet(); + + expect(result).toEqual({ + address: '0xDEF456', + chainId: 137, + }); + }); + + it('throws when window.ethereum is not present', async () => { + const original = window.ethereum; + // @ts-expect-error testing missing ethereum + window.ethereum = undefined; + + const { connectCoinbaseWallet } = await loadCoinbase(); + await expect(connectCoinbaseWallet()).rejects.toThrow( + 'Coinbase Wallet is not installed', + ); + + window.ethereum = original; + }); + + it('throws when Coinbase Wallet is not the active provider', async () => { + mockEthereum.isCoinbaseWallet = false; + + const { connectCoinbaseWallet } = await loadCoinbase(); + await expect(connectCoinbaseWallet()).rejects.toThrow( + 'Coinbase Wallet extension not detected', + ); + }); + + it('throws when no accounts are returned', async () => { + mockEthereum.request.mockResolvedValueOnce([]); + + const { connectCoinbaseWallet } = await loadCoinbase(); + await expect(connectCoinbaseWallet()).rejects.toThrow( + 'No accounts returned from Coinbase Wallet', + ); + }); + + it('handles user rejection (error code 4001)', async () => { + mockEthereum.request.mockRejectedValueOnce({ code: 4001 }); + + const { connectCoinbaseWallet } = await loadCoinbase(); + await expect(connectCoinbaseWallet()).rejects.toThrow( + 'You rejected the connection request', + ); + }); + + it('handles pending request error (code -32002)', async () => { + mockEthereum.request.mockRejectedValueOnce({ code: -32002 }); + + const { connectCoinbaseWallet } = await loadCoinbase(); + await expect(connectCoinbaseWallet()).rejects.toThrow( + 'Coinbase Wallet connection request is already pending', + ); + }); + + it('isCoinbaseAvailable returns false when window.ethereum is missing', async () => { + const original = window.ethereum; + // @ts-expect-error testing missing ethereum + window.ethereum = undefined; + + const { isCoinbaseAvailable } = await loadCoinbase(); + expect(isCoinbaseAvailable()).toBe(false); + + window.ethereum = original; + }); + + it('isCoinbaseAvailable returns true when Coinbase Wallet is present', async () => { + mockEthereum.isCoinbaseWallet = true; + + const { isCoinbaseAvailable } = await loadCoinbase(); + expect(isCoinbaseAvailable()).toBe(true); + }); +}); + +// ─── WalletConnect ─────────────────────────────────────────────────────────── + +// Controllable mock provider for WalletConnect tests +let mockWalletConnectProvider: { + enable: jest.Mock; + request: jest.Mock; +}; + +jest.mock('@walletconnect/web3-provider', () => { + return jest.fn().mockImplementation(() => mockWalletConnectProvider); +}); + +describe('WalletConnect connector', () => { + let walletconnectModule: typeof import('../walletconnect'); + + beforeEach(() => { + mockWalletConnectProvider = { + enable: jest.fn(), + request: jest.fn(), + }; + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID = 'test-wc-id'; + + // Fresh import for each test using isolateModules + jest.isolateModules(() => { + // eslint-disable-next-line @typescript-eslint/no-require-imports + walletconnectModule = require('../walletconnect'); + }); + }); + + afterEach(() => { + delete process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + }); + + it('isWalletConnectConfigured returns true when project ID is set', () => { + process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID = 'valid-id'; + expect(walletconnectModule.isWalletConnectConfigured()).toBe(true); + }); + + it('isWalletConnectConfigured returns false when project ID is missing', () => { + delete process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID; + expect(walletconnectModule.isWalletConnectConfigured()).toBe(false); + }); + + it('connectWalletConnectWallet throws when provider returns no accounts', async () => { + mockWalletConnectProvider.enable.mockResolvedValue([]); + + // The 'No accounts returned' error is thrown inside the try block + // but the catch block re-throws it as the generic error since the + // message doesn't match the specific error handling cases. + await expect( + walletconnectModule.connectWalletConnectWallet(), + ).rejects.toThrow('Failed to connect with WalletConnect'); + }); + + it('connectWalletConnectWallet returns address and chainId on success', async () => { + mockWalletConnectProvider.enable.mockResolvedValue(['0xWALLET1']); + mockWalletConnectProvider.request.mockResolvedValue('0x1'); + + const result = + await walletconnectModule.connectWalletConnectWallet(); + + expect(result).toEqual({ + address: '0xWALLET1', + chainId: 1, + }); + }); + + it('connectWalletConnectWallet handles user rejection', async () => { + mockWalletConnectProvider.enable.mockRejectedValue( + new Error('User rejected'), + ); + + await expect( + walletconnectModule.connectWalletConnectWallet(), + ).rejects.toThrow('You rejected the WalletConnect request'); + }); + + it('connectWalletConnectWallet handles project ID errors', async () => { + mockWalletConnectProvider.enable.mockRejectedValue( + new Error('project ID missing'), + ); + + await expect( + walletconnectModule.connectWalletConnectWallet(), + ).rejects.toThrow('WalletConnect is not properly configured'); + }); + + it('connectWalletConnectWallet handles generic errors', async () => { + mockWalletConnectProvider.enable.mockRejectedValue( + new Error('network timeout'), + ); + + await expect( + walletconnectModule.connectWalletConnectWallet(), + ).rejects.toThrow('Failed to connect with WalletConnect'); + }); +});