From b21a251302c7a5f057f9b47f58ae4bc38e11982b Mon Sep 17 00:00:00 2001 From: Alex Luu Date: Fri, 14 Jun 2024 21:13:46 -0400 Subject: [PATCH 1/4] add geth tests --- packages/web3-providers-ws/package.json | 3 +- .../ganache_fault_tolerance.test.ts | 3 +- .../integration/geth_fault_tolerance.test.ts | 305 ++++++++++++++++++ scripts/geth.sh | 4 + 4 files changed, 312 insertions(+), 3 deletions(-) create mode 100644 packages/web3-providers-ws/test/integration/geth_fault_tolerance.test.ts diff --git a/packages/web3-providers-ws/package.json b/packages/web3-providers-ws/package.json index b3612afaa23..59153fda4b9 100644 --- a/packages/web3-providers-ws/package.json +++ b/packages/web3-providers-ws/package.json @@ -58,7 +58,8 @@ "jest-extended": "^3.0.1", "prettier": "^2.7.1", "ts-jest": "^29.1.1", - "typescript": "^4.7.4" + "typescript": "^4.7.4", + "web3-providers-http": "^4.1.0" }, "dependencies": { "@types/ws": "8.5.3", diff --git a/packages/web3-providers-ws/test/integration/ganache_fault_tolerance.test.ts b/packages/web3-providers-ws/test/integration/ganache_fault_tolerance.test.ts index 30078527ca2..f912e9b89ce 100644 --- a/packages/web3-providers-ws/test/integration/ganache_fault_tolerance.test.ts +++ b/packages/web3-providers-ws/test/integration/ganache_fault_tolerance.test.ts @@ -32,7 +32,6 @@ import WebSocketProvider from '../../src/index'; // create helper functions to open server describeIf(getSystemTestBackend() === 'ganache' && isWs)('ganache tests', () => { describe('WebSocketProvider - ganache', () => { - jest.setTimeout(17000); const port = 7547; const host = `ws://localhost:${port}`; const jsonRpcPayload = { @@ -85,7 +84,7 @@ describeIf(getSystemTestBackend() === 'ganache' && isWs)('ganache tests', () => const disconnectPromise = waitForEvent(webSocketProvider, 'disconnect'); await server.close(); - expect(!!(await disconnectPromise)).toBe(true); + expect((await disconnectPromise)).toBe(true); webSocketProvider.disconnect(); }); diff --git a/packages/web3-providers-ws/test/integration/geth_fault_tolerance.test.ts b/packages/web3-providers-ws/test/integration/geth_fault_tolerance.test.ts new file mode 100644 index 00000000000..3637565081d --- /dev/null +++ b/packages/web3-providers-ws/test/integration/geth_fault_tolerance.test.ts @@ -0,0 +1,305 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { + HttpProvider +} from 'web3-providers-http'; +import { ConnectionNotOpenError } from 'web3-errors'; +import { EthExecutionAPI, Web3APIPayload, SocketRequestItem, JsonRpcResponse, ProviderRpcError } from 'web3-types'; +import { Web3DeferredPromise } from 'web3-utils'; +import { + waitForSocketConnect, + waitForEvent, +} from '../fixtures/system_test_utils'; +import { WebSocketProvider } from '../../src'; + + + +// create helper functions to open server +describe('geth tests', () => { + const wsProviderUrl = 'ws://127.0.0.1:3333'; + const httpProviderUrl = 'http://127.0.0.1:3333'; + let httpProvider: HttpProvider; + const openServer = async () => { + await httpProvider.request({ + method: 'admin_startWS', + id: '1', + jsonrpc: '2.0' + }) + } + const closeServer = async () => { + await httpProvider.request({ + method: 'admin_stopWS', + id: '2', + jsonrpc: '2.0' + }); + }; + const jsonRpcPayload = { + jsonrpc: '2.0', + id: 43, + method: 'eth_mining', + } as Web3APIPayload; + + // simulate abrupt disconnection, ganache server always closes with code 1000 so we need to simulate closing with different error code + const changeCloseCode = async (webSocketProvider: WebSocketProvider) => + new Promise(resolve => { + // @ts-expect-error replace close handler + // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-param-reassign + webSocketProvider._onCloseHandler = (_: CloseEvent) => { + // @ts-expect-error replace close event + webSocketProvider._onCloseEvent({ code: 1003 }); + }; + // @ts-expect-error run protected method + webSocketProvider._removeSocketListeners(); + // @ts-expect-error run protected method + webSocketProvider._addSocketListeners(); + resolve(); + }); + + beforeAll(() => { + httpProvider = new HttpProvider(httpProviderUrl); + }) + beforeEach(async () => { + await openServer(); + }) + // opens ws server port + + + describe('WebSocketProvider fault tests - geth', () => { + it('"error" when there is no connection', async () => { + const reconnectionOptions = { + delay: 100, + autoReconnect: false, + maxAttempts: 1, + }; + const websocketProvider = new WebSocketProvider( + 'ws://localhost:7547', + {}, + reconnectionOptions, + ); + + expect(!!(await waitForEvent(websocketProvider, 'error'))).toBe(true); + websocketProvider.disconnect(); + await expect(websocketProvider.request(jsonRpcPayload)).rejects.toThrow( + 'Connection not open', + ); + }); + + it('"discconect" handler fires if the server closes', async () => { + await openServer(); + const err = jest.fn(); + const webSocketProvider = new WebSocketProvider(wsProviderUrl, {}, { + delay: 100, + autoReconnect: false, + maxAttempts: 1, + }); + + await waitForSocketConnect(webSocketProvider); + + webSocketProvider.on('disconnect', () => { + err(); + }); + const errorPromise = waitForEvent(webSocketProvider, 'disconnect'); + // await server.close(); + await closeServer(); + await errorPromise; + expect(err).toHaveBeenCalled(); + }); + it('"error" handler *DOES NOT* fire if disconnection is clean', async () => { + await openServer(); + const reconnectOptions = { + autoReconnect: false, + }; + const webSocketProvider = new WebSocketProvider(wsProviderUrl, {}, reconnectOptions); + await waitForSocketConnect(webSocketProvider); + + const mockReject = jest.fn(); + const mockDisconnect = jest.fn(); + webSocketProvider.once('error', () => { + mockReject(); + }); + webSocketProvider.once('disconnect', () => { + mockDisconnect(); + }) + webSocketProvider.disconnect(); + expect(mockReject).toHaveBeenCalledTimes(0); + expect(mockDisconnect).toHaveBeenCalledTimes(1); + }); + it('can connect after being disconnected', async () => { + await openServer(); + + const webSocketProvider = new WebSocketProvider(wsProviderUrl); + const mockCallback = jest.fn(); + const connectPromise = new Promise(resolve => { + webSocketProvider.once('connect', () => { + mockCallback(); + resolve(true); + }); + }); + await connectPromise; + + webSocketProvider.disconnect(); + await waitForEvent(webSocketProvider, 'disconnect'); + + webSocketProvider.connect(); + const connectPromise2 = new Promise(resolve => { + webSocketProvider.once('connect', () => { + mockCallback(); + resolve(true); + }); + }); + await connectPromise2; + webSocketProvider.disconnect(); + expect(mockCallback).toHaveBeenCalledTimes(2); + }); + it('webSocketProvider supports subscriptions', async () => { + const webSocketProvider = new WebSocketProvider(wsProviderUrl); + + await waitForSocketConnect(webSocketProvider); + expect(webSocketProvider.supportsSubscriptions()).toBe(true); + + webSocketProvider.disconnect(); + }); + + it('times out when server is closed', async () => { + const reconnectionOptions = { + delay: 100, + autoReconnect: false, + maxAttempts: 1, + }; + const webSocketProvider = new WebSocketProvider(wsProviderUrl, {}, reconnectionOptions); + const mockCallBack = jest.fn(); + const errorPromise = new Promise(resolve => { + webSocketProvider.on('error', () => { + mockCallBack(); + resolve(true); + }); + }); + await closeServer(); + await errorPromise; + expect(mockCallBack).toHaveBeenCalled(); + }); + it('with reconnect on, will try to connect until server is open then close', async () => { + await closeServer(); + const reconnectionOptions = { + delay: 10, + autoReconnect: true, + maxAttempts: 100, + }; + const webSocketProvider = new WebSocketProvider(wsProviderUrl, {}, reconnectionOptions); + + const mockCallback = jest.fn(); + const connectPromise = new Promise(resolve => { + webSocketProvider.on('connect', () => { + mockCallback(); + resolve(true); + }); + }); + await openServer(); + await connectPromise; + webSocketProvider.disconnect(); + expect(mockCallback).toHaveBeenCalledTimes(1); + }); + + it('allows disconnection on lost connection, when reconnect is enabled', async () => { + const reconnectionOptions = { + delay: 10, + autoReconnect: true, + maxAttempts: 100, + }; + const webSocketProvider = new WebSocketProvider(wsProviderUrl, {}, reconnectionOptions); + + const connectPromise = waitForSocketConnect(webSocketProvider); + + await connectPromise; + const disconnectEvent = waitForEvent(webSocketProvider, 'disconnect'); + await closeServer(); + webSocketProvider.disconnect(); + expect(!!(await disconnectEvent)).toBe(true); + }); + + it('errors when failing to reconnect after data is lost mid-chunk', async () => { + const reconnectionOptions = { + delay: 100, + autoReconnect: true, + maxAttempts: 1, + }; + const mockCallBack = jest.fn(); + const webSocketProvider = new WebSocketProvider(wsProviderUrl, {}, reconnectionOptions); + await waitForSocketConnect(webSocketProvider); + + webSocketProvider.on('error', (err: any) => { + if (err.message === `Maximum number of reconnect attempts reached! (${1})`) { + mockCallBack(); + } + }); + + // await server.close(); + await closeServer(); + + // when server is not listening send request, and expect that lib will try to reconnect and at end will throw con not open error + await expect( + webSocketProvider.request( + {"method":"eth_getBlockByNumber","params":["0xc5043f",false],"id":1,"jsonrpc":"2.0"} + )) + .rejects.toThrow(ConnectionNotOpenError); + + expect(mockCallBack).toHaveBeenCalled(); + }); + it('clears pending requests on maxAttempts failed reconnection', async () => { + const reconnectionOptions = { + delay: 1000, + autoReconnect: true, + maxAttempts: 1, + }; + const mockCallBack = jest.fn(); + + const webSocketProvider = new WebSocketProvider(wsProviderUrl, {}, reconnectionOptions); + const defPromise = new Web3DeferredPromise>(); + // eslint-disable-next-line @typescript-eslint/no-empty-function + defPromise.catch(() => {}); + const reqItem: SocketRequestItem = { + payload: jsonRpcPayload, + deferredPromise: defPromise, + }; + await waitForSocketConnect(webSocketProvider); + + // add a request without executing promise + // @ts-expect-error run protected method + webSocketProvider._pendingRequestsQueue.set(jsonRpcPayload.id, reqItem); + + // simulate abrupt server close + await changeCloseCode(webSocketProvider); + const errorPromise = new Promise(resolve => { + webSocketProvider.on('error', (error: unknown) => { + if ( + (error as ProviderRpcError)?.message === + `Maximum number of reconnect attempts reached! (${1})` + ) { + mockCallBack(); + } + resolve(true); + }); + }); + await closeServer(); + await errorPromise; + // @ts-expect-error run protected method + expect(webSocketProvider._pendingRequestsQueue.size).toBe(0); + expect(mockCallBack).toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file diff --git a/scripts/geth.sh b/scripts/geth.sh index 62c7213ccfb..3d4cbd5173d 100755 --- a/scripts/geth.sh +++ b/scripts/geth.sh @@ -20,6 +20,9 @@ start() { docker run -d -p $WEB3_SYSTEM_TEST_PORT:$WEB3_SYSTEM_TEST_PORT ethereum/client-go:v1.13.14-amd64 --nodiscover --nousb --ws --ws.addr 0.0.0.0 --ws.port $WEB3_SYSTEM_TEST_PORT --http --http.addr 0.0.0.0 --http.port $WEB3_SYSTEM_TEST_PORT --allow-insecure-unlock --http.api personal,web3,eth,admin,debug,txpool,net --ws.api personal,web3,eth,admin,debug,miner,txpool,net --dev echo "Waiting for geth..." npx wait-port -t 10000 "$WEB3_SYSTEM_TEST_PORT" + echo "docker run -d -p 3333:3333 ethereum/client-go:v1.13.14-amd64 --nodiscover --nousb --ws --ws.addr 0.0.0.0 --ws.port 3333 --http --http.addr 0.0.0.0 --http.port 3334 --allow-insecure-unlock --http.api personal,web3,eth,admin,debug,txpool,net --ws.api personal,web3,eth,admin,debug,miner,txpool,net --dev" + docker run -d -p 3333:3333 ethereum/client-go:v1.13.14-amd64 --nodiscover --nousb --ws --ws.addr 0.0.0.0 --ws.port 3333 --http --http.addr 0.0.0.0 --http.port 3333 --allow-insecure-unlock --http.api personal,web3,eth,admin,debug,txpool,net --ws.api personal,web3,eth,admin,debug,miner,txpool,net --dev + npx wait-port -t 10000 3333 echo "Geth started" fi } @@ -27,6 +30,7 @@ start() { stop() { echo "Stopping geth ..." docker ps -q --filter ancestor="ethereum/client-go" | xargs -r docker stop + docker ps -a -q --filter ancestor="ethereum/client-go" | xargs -r docker rm } case $1 in From 82f4bed17319256eb2fbfb06b9b7479ff7ef2c23 Mon Sep 17 00:00:00 2001 From: Alex Luu Date: Fri, 14 Jun 2024 21:15:48 -0400 Subject: [PATCH 2/4] update tests --- .../test/integration/geth_fault_tolerance.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/web3-providers-ws/test/integration/geth_fault_tolerance.test.ts b/packages/web3-providers-ws/test/integration/geth_fault_tolerance.test.ts index 3637565081d..7d279d323b4 100644 --- a/packages/web3-providers-ws/test/integration/geth_fault_tolerance.test.ts +++ b/packages/web3-providers-ws/test/integration/geth_fault_tolerance.test.ts @@ -24,13 +24,16 @@ import { Web3DeferredPromise } from 'web3-utils'; import { waitForSocketConnect, waitForEvent, + describeIf, + getSystemTestBackend, + isWs, } from '../fixtures/system_test_utils'; import { WebSocketProvider } from '../../src'; // create helper functions to open server -describe('geth tests', () => { +describeIf(getSystemTestBackend() === 'ganache' && isWs)('ganache tests', () => { const wsProviderUrl = 'ws://127.0.0.1:3333'; const httpProviderUrl = 'http://127.0.0.1:3333'; let httpProvider: HttpProvider; From f1015b86d52c188b950bbe59b8a56b408b45625f Mon Sep 17 00:00:00 2001 From: Alex Luu Date: Mon, 17 Jun 2024 16:26:16 -0400 Subject: [PATCH 3/4] update --- .../ganache_fault_tolerance.test.ts | 414 ------------------ .../integration/geth_fault_tolerance.test.ts | 118 ++++- 2 files changed, 112 insertions(+), 420 deletions(-) delete mode 100644 packages/web3-providers-ws/test/integration/ganache_fault_tolerance.test.ts diff --git a/packages/web3-providers-ws/test/integration/ganache_fault_tolerance.test.ts b/packages/web3-providers-ws/test/integration/ganache_fault_tolerance.test.ts deleted file mode 100644 index f912e9b89ce..00000000000 --- a/packages/web3-providers-ws/test/integration/ganache_fault_tolerance.test.ts +++ /dev/null @@ -1,414 +0,0 @@ -/* -This file is part of web3.js. - -web3.js is free software: you can redistribute it and/or modify -it under the terms of the GNU Lesser General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -web3.js is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public License -along with web3.js. If not, see . -*/ - -import { ProviderRpcError } from 'web3-types/src/web3_api_types'; -import ganache from 'ganache'; -import { EthExecutionAPI, Web3APIPayload, SocketRequestItem, JsonRpcResponse } from 'web3-types'; -import { InvalidResponseError, ConnectionNotOpenError } from 'web3-errors'; -import { Web3DeferredPromise } from 'web3-utils'; -import { - waitForSocketConnect, - waitForEvent, - describeIf, - getSystemTestBackend, - isWs, -} from '../fixtures/system_test_utils'; -import WebSocketProvider from '../../src/index'; - -// create helper functions to open server -describeIf(getSystemTestBackend() === 'ganache' && isWs)('ganache tests', () => { - describe('WebSocketProvider - ganache', () => { - const port = 7547; - const host = `ws://localhost:${port}`; - const jsonRpcPayload = { - jsonrpc: '2.0', - id: 43, - method: 'eth_mining', - } as Web3APIPayload; - - // simulate abrupt disconnection, ganache server always closes with code 1000 so we need to simulate closing with different error code - const changeCloseCode = async (webSocketProvider: WebSocketProvider) => - new Promise(resolve => { - // @ts-expect-error replace close handler - // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-param-reassign - webSocketProvider._onCloseHandler = (_: CloseEvent) => { - // @ts-expect-error replace close event - webSocketProvider._onCloseEvent({ code: 1003 }); - }; - // @ts-expect-error run protected method - webSocketProvider._removeSocketListeners(); - // @ts-expect-error run protected method - webSocketProvider._addSocketListeners(); - resolve(); - }); - - it('"error" when there is no connection', async () => { - const reconnectionOptions = { - delay: 100, - autoReconnect: false, - maxAttempts: 1, - }; - const websocketProvider = new WebSocketProvider( - 'ws://localhost:7547', - {}, - reconnectionOptions, - ); - - expect(!!(await waitForEvent(websocketProvider, 'error'))).toBe(true); - websocketProvider.disconnect(); - await expect(websocketProvider.request(jsonRpcPayload)).rejects.toThrow( - 'Connection not open', - ); - }); - - it('"error" handler fires if the client closes unilaterally', async () => { - const server = ganache.server(); - await server.listen(port); - const webSocketProvider = new WebSocketProvider(host); - - await waitForSocketConnect(webSocketProvider); - - const disconnectPromise = waitForEvent(webSocketProvider, 'disconnect'); - await server.close(); - expect((await disconnectPromise)).toBe(true); - webSocketProvider.disconnect(); - }); - - it('"error" handler *DOES NOT* fire if disconnection is clean', async () => { - const server = ganache.server(); - await server.listen(port); - const reconnectOptions = { - autoReconnect: false, - }; - const webSocketProvider = new WebSocketProvider(host, {}, reconnectOptions); - await waitForSocketConnect(webSocketProvider); - - const mockReject = jest.fn(); - webSocketProvider.once('error', () => { - mockReject(); - }); - webSocketProvider.disconnect(); - await new Promise(resolve => { - setTimeout(() => { - resolve(true); - }, 100); - }); - expect(mockReject).toHaveBeenCalledTimes(0); - - await server.close(); - }); - - it('can connect after being disconnected', async () => { - const server = ganache.server(); - await server.listen(port); - - const webSocketProvider = new WebSocketProvider(host); - const mockCallback = jest.fn(); - const connectPromise = new Promise(resolve => { - webSocketProvider.once('connect', () => { - mockCallback(); - resolve(true); - }); - }); - await connectPromise; - - webSocketProvider.disconnect(); - await waitForEvent(webSocketProvider, 'disconnect'); - - webSocketProvider.connect(); - const connectPromise2 = new Promise(resolve => { - webSocketProvider.once('connect', () => { - mockCallback(); - resolve(true); - }); - }); - await connectPromise2; - webSocketProvider.disconnect(); - expect(mockCallback).toHaveBeenCalledTimes(2); - await server.close(); - }); - - it('webSocketProvider supports subscriptions', async () => { - const server = ganache.server(); - await server.listen(port); - const webSocketProvider = new WebSocketProvider(host); - - await waitForSocketConnect(webSocketProvider); - expect(webSocketProvider.supportsSubscriptions()).toBe(true); - - webSocketProvider.disconnect(); - await server.close(); - }); - - it('times out when server is closed', async () => { - const server = ganache.server(); - await server.listen(port); - const reconnectionOptions = { - delay: 100, - autoReconnect: false, - maxAttempts: 1, - }; - const webSocketProvider = new WebSocketProvider(host, {}, reconnectionOptions); - const mockCallBack = jest.fn(); - const errorPromise = new Promise(resolve => { - webSocketProvider.on('error', (err: unknown) => { - if ((err as ProviderRpcError)?.message.startsWith('connect ECONNREFUSED')) { - mockCallBack(); - resolve(true); - } - }); - }); - await server.close(); - await errorPromise; - expect(mockCallBack).toHaveBeenCalled(); - }); - - it('with reconnect on, will try to connect until server is open then close', async () => { - const reconnectionOptions = { - delay: 10, - autoReconnect: true, - maxAttempts: 100, - }; - const webSocketProvider = new WebSocketProvider(host, {}, reconnectionOptions); - - const mockCallback = jest.fn(); - const connectPromise = new Promise(resolve => { - webSocketProvider.on('connect', () => { - mockCallback(); - resolve(true); - }); - }); - - const server = ganache.server(); - await server.listen(port); - await connectPromise; - webSocketProvider.disconnect(); - await server.close(); - expect(mockCallback).toHaveBeenCalledTimes(1); - }); - - it('allows disconnection on lost connection, when reconnect is enabled', async () => { - const reconnectionOptions = { - delay: 10, - autoReconnect: true, - maxAttempts: 100, - }; - const webSocketProvider = new WebSocketProvider(host, {}, reconnectionOptions); - - const connectPromise = waitForSocketConnect(webSocketProvider); - - const server = ganache.server(); - await server.listen(port); - await connectPromise; - await server.close(); - const disconnectEvent = waitForEvent(webSocketProvider, 'disconnect'); - webSocketProvider.disconnect(); - expect(!!(await disconnectEvent)).toBe(true); - }); - - it('errors when failing to reconnect after data is lost mid-chunk', async () => { - const server = ganache.server(); - await server.listen(port); - const reconnectionOptions = { - delay: 100, - autoReconnect: true, - maxAttempts: 1, - }; - const mockCallBack = jest.fn(); - const webSocketProvider = new WebSocketProvider(host, {}, reconnectionOptions); - await waitForSocketConnect(webSocketProvider); - - webSocketProvider.on('error', (err: any) => { - if (err.message === `Maximum number of reconnect attempts reached! (${1})`) { - mockCallBack(); - } - }); - - await server.close(); - - // when server is not listening send request, and expect that lib will try to reconnect and at end will throw con not open error - await expect( - webSocketProvider.request( - {"method":"eth_getBlockByNumber","params":["0xc5043f",false],"id":1,"jsonrpc":"2.0"} - )) - .rejects.toThrow(ConnectionNotOpenError); - - expect(mockCallBack).toHaveBeenCalled(); - }); - - it('times out when connection is lost mid-chunk', async () => { - const server = ganache.server(); - await server.listen(port); - const reconnectionOptions = { - delay: 0, - autoReconnect: false, - maxAttempts: 0, - }; - const webSocketProvider = new WebSocketProvider(host, {}, reconnectionOptions); - await waitForSocketConnect(webSocketProvider); - - await server.close(); - - const errorPromise = new Promise(resolve => { - webSocketProvider.on('error', (err: any) => { - expect(err).toBeInstanceOf(InvalidResponseError); - if (err.cause.message === 'Chunk timeout') { - resolve(true); - } - }); - }); - // send an event to be parsed and fail - const event = { - data: 'abc|--|ded', - type: 'websocket', - // @ts-expect-error run protected method - target: webSocketProvider._socketConnection, - }; - // @ts-expect-error run protected method - webSocketProvider._parseResponses(event); // simulate chunks - await errorPromise; - expect(true).toBe(true); - }); - - it('clears pending requests on maxAttempts failed reconnection', async () => { - const server = ganache.server(); - await server.listen(port); - const reconnectionOptions = { - delay: 1000, - autoReconnect: true, - maxAttempts: 1, - }; - const mockCallBack = jest.fn(); - - const webSocketProvider = new WebSocketProvider(host, {}, reconnectionOptions); - const defPromise = new Web3DeferredPromise>(); - // eslint-disable-next-line @typescript-eslint/no-empty-function - defPromise.catch(() => {}); - const reqItem: SocketRequestItem = { - payload: jsonRpcPayload, - deferredPromise: defPromise, - }; - await waitForSocketConnect(webSocketProvider); - - // add a request without executing promise - // @ts-expect-error run protected method - webSocketProvider._pendingRequestsQueue.set(jsonRpcPayload.id, reqItem); - - // simulate abrupt server close - await changeCloseCode(webSocketProvider); - const errorPromise = new Promise(resolve => { - webSocketProvider.on('error', (error: unknown) => { - if ( - (error as ProviderRpcError)?.message === - `Maximum number of reconnect attempts reached! (${1})` - ) { - mockCallBack(); - } - resolve(true); - }); - }); - - await server.close(); - await errorPromise; - // @ts-expect-error run protected method - expect(webSocketProvider._pendingRequestsQueue.size).toBe(0); - expect(mockCallBack).toHaveBeenCalled(); - }); - - it('queues requests made while connection is lost / executes on reconnect', async () => { - const server = ganache.server(); - await server.listen(port); - const reconnectionOptions = { - delay: 1000, - autoReconnect: true, - maxAttempts: 3, - }; - const webSocketProvider = new WebSocketProvider(host, {}, reconnectionOptions); - await waitForSocketConnect(webSocketProvider); - - // simulate abrupt close code - await changeCloseCode(webSocketProvider); - const errorPromise = new Promise(resolve => { - webSocketProvider.on('error', () => { - resolve(true); - }); - }); - await server.close(); - - await errorPromise; - // queue a request - const requestPromise = webSocketProvider.request(jsonRpcPayload); - - const server2 = ganache.server(); - await server2.listen(port); - - await waitForSocketConnect(webSocketProvider); - - // try to send a request - const result = await requestPromise; - expect(result.id).toEqual(jsonRpcPayload.id); - webSocketProvider.disconnect(); - await server2.close(); - }); - it('errors when requests continue after socket closed', async () => { - const server = ganache.server(); - await server.listen(port); - const reconnectOptions = { - autoReconnect: false, - }; - const webSocketProvider = new WebSocketProvider(host, {}, reconnectOptions); - await waitForSocketConnect(webSocketProvider); - - const disconnectPromise = waitForEvent(webSocketProvider, 'disconnect'); - await server.close(); - - await disconnectPromise; - const errorPromise = new Promise(resolve => { - webSocketProvider.on('error', () => { - resolve(true); - }); - }); - await expect(webSocketProvider.request(jsonRpcPayload)).rejects.toThrow( - 'Connection not open', - ); - await errorPromise; - }); - it('deferredPromise emits an error when request fails', async () => { - const server = ganache.server(); - await server.listen(port); - const webSocketProvider = new WebSocketProvider(host); - await waitForSocketConnect(webSocketProvider); - - // @ts-expect-error replace sendtoSocket so we don't execute request - // eslint-disable-next-line @typescript-eslint/no-empty-function - webSocketProvider._sendToSocket = () => {}; - webSocketProvider.on('error', (err: unknown) => { - expect(err).toBeInstanceOf(ConnectionNotOpenError); - }); - - // eslint-disable-next-line @typescript-eslint/no-empty-function - const request = webSocketProvider.request(jsonRpcPayload).catch(() => {}); - - // @ts-expect-error create a deferred promise error - webSocketProvider._clearQueues(); - - await request; - webSocketProvider.disconnect(); - await server.close(); - }); - }); -}); diff --git a/packages/web3-providers-ws/test/integration/geth_fault_tolerance.test.ts b/packages/web3-providers-ws/test/integration/geth_fault_tolerance.test.ts index 7d279d323b4..a047666dccb 100644 --- a/packages/web3-providers-ws/test/integration/geth_fault_tolerance.test.ts +++ b/packages/web3-providers-ws/test/integration/geth_fault_tolerance.test.ts @@ -30,10 +30,7 @@ import { } from '../fixtures/system_test_utils'; import { WebSocketProvider } from '../../src'; - - -// create helper functions to open server -describeIf(getSystemTestBackend() === 'ganache' && isWs)('ganache tests', () => { +describeIf(getSystemTestBackend() === 'geth' && isWs)('geth tests', () => { const wsProviderUrl = 'ws://127.0.0.1:3333'; const httpProviderUrl = 'http://127.0.0.1:3333'; let httpProvider: HttpProvider; @@ -79,7 +76,9 @@ describeIf(getSystemTestBackend() === 'ganache' && isWs)('ganache tests', () => beforeEach(async () => { await openServer(); }) - // opens ws server port + afterAll(async() => { + await closeServer(); + }) describe('WebSocketProvider fault tests - geth', () => { @@ -121,6 +120,7 @@ describeIf(getSystemTestBackend() === 'ganache' && isWs)('ganache tests', () => await closeServer(); await errorPromise; expect(err).toHaveBeenCalled(); + webSocketProvider.disconnect(); }); it('"error" handler *DOES NOT* fire if disconnection is clean', async () => { await openServer(); @@ -177,7 +177,37 @@ describeIf(getSystemTestBackend() === 'ganache' && isWs)('ganache tests', () => webSocketProvider.disconnect(); }); - + // TODO: investigate this test + // it('times out when connection is lost mid-chunk', async () => { + // const reconnectionOptions = { + // delay: 0, + // autoReconnect: false, + // maxAttempts: 0, + // }; + // const webSocketProvider = new WebSocketProvider(wsProviderUrl, {}, reconnectionOptions); + // await waitForSocketConnect(webSocketProvider); + // await closeServer(); + + // const errorPromise = new Promise(resolve => { + // webSocketProvider.on('error', (err: any) => { + // expect(err).toBeInstanceOf(Error); + // if (err.cause.message === 'Chunk timeout') { + // resolve(true); + // } + // }); + // }); + // // send an event to be parsed and fail + // const event = { + // data: 'abc|--|ded', + // type: 'websocket', + // // @ts-expect-error run protected method + // target: webSocketProvider._socketConnection, + // }; + // // @ts-expect-error run protected method + // webSocketProvider._parseResponses(event); // simulate chunks + // await errorPromise; + // expect(true).toBe(true); + // }); it('times out when server is closed', async () => { const reconnectionOptions = { delay: 100, @@ -195,6 +225,7 @@ describeIf(getSystemTestBackend() === 'ganache' && isWs)('ganache tests', () => await closeServer(); await errorPromise; expect(mockCallBack).toHaveBeenCalled(); + webSocketProvider.disconnect(); }); it('with reconnect on, will try to connect until server is open then close', async () => { await closeServer(); @@ -262,6 +293,7 @@ describeIf(getSystemTestBackend() === 'ganache' && isWs)('ganache tests', () => .rejects.toThrow(ConnectionNotOpenError); expect(mockCallBack).toHaveBeenCalled(); + webSocketProvider.disconnect(); }); it('clears pending requests on maxAttempts failed reconnection', async () => { const reconnectionOptions = { @@ -303,6 +335,80 @@ describeIf(getSystemTestBackend() === 'ganache' && isWs)('ganache tests', () => // @ts-expect-error run protected method expect(webSocketProvider._pendingRequestsQueue.size).toBe(0); expect(mockCallBack).toHaveBeenCalled(); + webSocketProvider.disconnect(); + }); + it('queues requests made while connection is lost / executes on reconnect', async () => { + const reconnectionOptions = { + delay: 1000, + autoReconnect: true, + maxAttempts: 3, + }; + const webSocketProvider = new WebSocketProvider(wsProviderUrl, {}, reconnectionOptions); + await waitForSocketConnect(webSocketProvider); + + // simulate abrupt close code + await changeCloseCode(webSocketProvider); + const errorPromise = new Promise(resolve => { + webSocketProvider.on('error', () => { + resolve(true); + }); + }); + await closeServer(); + + await errorPromise; + // queue a request + const requestPromise = webSocketProvider.request(jsonRpcPayload); + + await openServer(); + + await waitForSocketConnect(webSocketProvider); + + // try to send a request + const result = await requestPromise; + expect(result.id).toEqual(jsonRpcPayload.id); + webSocketProvider.disconnect(); + }); + it('errors when requests continue after socket closed', async () => { + const reconnectOptions = { + autoReconnect: false, + }; + const webSocketProvider = new WebSocketProvider(wsProviderUrl, {}, reconnectOptions); + await waitForSocketConnect(webSocketProvider); + + const disconnectPromise = waitForEvent(webSocketProvider, 'disconnect'); + await closeServer(); + + await disconnectPromise; + const errorPromise = new Promise(resolve => { + webSocketProvider.on('error', () => { + resolve(true); + }); + }); + await expect(webSocketProvider.request(jsonRpcPayload)).rejects.toThrow( + 'Connection not open', + ); + await errorPromise; + }); + it('deferredPromise emits an error when request fails', async () => { + const webSocketProvider = new WebSocketProvider(wsProviderUrl); + await waitForSocketConnect(webSocketProvider); + + // @ts-expect-error replace sendtoSocket so we don't execute request + // eslint-disable-next-line @typescript-eslint/no-empty-function + webSocketProvider._sendToSocket = () => {}; + webSocketProvider.on('error', (err: unknown) => { + expect(err).toBeInstanceOf(ConnectionNotOpenError); + }); + + // eslint-disable-next-line @typescript-eslint/no-empty-function + const request = webSocketProvider.request(jsonRpcPayload).catch(() => {}); + + // @ts-expect-error create a deferred promise error + webSocketProvider._clearQueues(); + + await request; + webSocketProvider.disconnect(); + await closeServer(); }); }); }); \ No newline at end of file From 230a36477046418eac8d49bd9a571d3b1c41b618 Mon Sep 17 00:00:00 2001 From: Alex Luu Date: Mon, 17 Jun 2024 22:11:04 -0400 Subject: [PATCH 4/4] update scripts --- scripts/geth.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/geth.sh b/scripts/geth.sh index 3d4cbd5173d..5459fc23d31 100755 --- a/scripts/geth.sh +++ b/scripts/geth.sh @@ -29,8 +29,7 @@ start() { stop() { echo "Stopping geth ..." - docker ps -q --filter ancestor="ethereum/client-go" | xargs -r docker stop - docker ps -a -q --filter ancestor="ethereum/client-go" | xargs -r docker rm + docker ps -a -q --filter ancestor="ethereum/client-go" | xargs -r docker stop } case $1 in