diff --git a/testmodule/providers/jsonrpc/mocks/mockWebSocketServer2.nim b/testmodule/providers/jsonrpc/mocks/mockWebSocketServer2.nim new file mode 100644 index 0000000..d7a0654 --- /dev/null +++ b/testmodule/providers/jsonrpc/mocks/mockWebSocketServer2.nim @@ -0,0 +1,38 @@ +import std/tables +import std/strutils +import std/uri + # pkg/chronos, +import pkg/chronicles + # pkg/chronos/apps/http/httpserver, +import pkg/websock/websock +import pkg/websock/tests/helpers +import pkg/httputils +import pkg/asynctest/chronos/unittest + # json_rpc/clients/websocketclient, + # json_rpc/[client, server], + # json_serialization + +import pkg/stew/byteutils +import pkg/ethers + +const address = initTAddress("127.0.0.1:8888") + +proc handle(request: HttpRequest) {.async.} = + check request.uri.path == WSPath + + let server = WSServer.new(protos = ["proto"]) + let ws = await server.handleRequest(request) + let servRes = await ws.recvMsg() + + check string.fromBytes(servRes) == testString + await ws.waitForClose() + + +proc run() {.async.} = + + let server = createServer( + address = address, + handler = handle, + flags = {ReuseAddr}) + + let provider = JsonRpcProvider.new("ws://" & $address) \ No newline at end of file diff --git a/testmodule/providers/jsonrpc/testErrors.nim b/testmodule/providers/jsonrpc/testErrors.nim index 6940dbf..0b6bd06 100644 --- a/testmodule/providers/jsonrpc/testErrors.nim +++ b/testmodule/providers/jsonrpc/testErrors.nim @@ -12,40 +12,38 @@ import pkg/ethers/providers/jsonrpc import pkg/ethers/providers/jsonrpc/errors import pkg/ethers/erc20 import pkg/json_rpc/clients/httpclient +import pkg/websock/websock import ./mocks/mockHttpServer +import ./mocks/mockWebSocketServer import ../../examples import ../../hardhat suite "JSON RPC errors": - test "converts JSON RPC error to Nim error": - let error = %*{ "message": "some error" } + let error = %*{"message": "some error"} check JsonRpcProviderError.new(error).msg == "some error" test "converts error data to bytes": - let error = %*{ - "message": "VM Exception: reverted with 'some error'", - "data": "0xabcd" - } + let error = + %*{"message": "VM Exception: reverted with 'some error'", "data": "0xabcd"} check JsonRpcProviderError.new(error).data == some @[0xab'u8, 0xcd'u8] test "converts nested error data to bytes": - let error = %*{ - "message": "VM Exception: reverted with 'some error'", - "data": { + let error = + %*{ "message": "VM Exception: reverted with 'some error'", - "data": "0xabcd" + "data": + {"message": "VM Exception: reverted with 'some error'", "data": "0xabcd"}, } - } check JsonRpcProviderError.new(error).data == some @[0xab'u8, 0xcd'u8] -type - TestToken = ref object of Erc20Token +type TestToken = ref object of Erc20Token -method mint(token: TestToken, holder: Address, amount: UInt256): Confirmable {.base, contract.} +method mint( + token: TestToken, holder: Address, amount: UInt256 +): Confirmable {.base, contract.} suite "Network errors - HTTP": - var provider: JsonRpcProvider var mockServer: MockHttpServer var token: TestToken @@ -72,20 +70,29 @@ suite "Network errors - HTTP": # mockServer.registerRpcResponse("eth_subscribe", response) # TODO: handle # eth_subscribe for websockets - proc testCustomResponse(errorName: string, responseHttpCode: HttpCode, responseText: string, errorType: type CatchableError) = - let response = proc(request: HttpRequestRef): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = + proc testCustomResponse( + errorName: string, + responseHttpCode: HttpCode, + responseText: string, + errorType: type CatchableError, + ) = + let response = proc( + request: HttpRequestRef + ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = try: return await request.respond(responseHttpCode, responseText) except HttpWriteError as exc: return defaultResponse(exc) - let testNamePrefix = errorName & " error response is converted to " & errorType.name & " for " + let testNamePrefix = + errorName & " error response is converted to " & errorType.name & " for " test testNamePrefix & "sending a manual RPC method request": registerRpcMethods(response) expect errorType: discard await provider.send("eth_accounts") - test testNamePrefix & "calling a provider method that converts errors when calling a generated RPC request": + test testNamePrefix & + "calling a provider method that converts errors when calling a generated RPC request": registerRpcMethods(response) expect errorType: discard await provider.listAccounts() @@ -100,8 +107,9 @@ suite "Network errors - HTTP": expect errorType: token = TestToken.new(token.address, provider.getSigner()) discard await token.mint( - Address.example, 100.u256, - TransactionOverrides(gasLimit: 100.u256.some, chainId: 1.u256.some) + Address.example, + 100.u256, + TransactionOverrides(gasLimit: 100.u256.some, chainId: 1.u256.some), ) test testNamePrefix & "sending a manual transaction": @@ -113,7 +121,8 @@ suite "Network errors - HTTP": test testNamePrefix & "sending a raw transaction": registerRpcMethods(response) expect errorType: - const pk_with_funds = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" + const pk_with_funds = + "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" let wallet = !Wallet.new(pk_with_funds) let tx = Transaction( to: wallet.address, @@ -128,14 +137,17 @@ suite "Network errors - HTTP": test testNamePrefix & "subscribing to blocks": registerRpcMethods(response) expect errorType: - let emptyHandler = proc(blckResult: ?!Block) = discard + let emptyHandler = proc(blckResult: ?!Block) = + discard discard await provider.subscribe(emptyHandler) test testNamePrefix & "subscribing to logs": registerRpcMethods(response) expect errorType: - let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example]) - let emptyHandler = proc(log: ?!Log) = discard + let filter = + EventFilter(address: Address.example, topics: @[array[32, byte].example]) + let emptyHandler = proc(log: ?!Log) = + discard discard await provider.subscribe(filter, emptyHandler) testCustomResponse("429", Http429, "Too many requests", HttpRequestLimitError) @@ -146,7 +158,9 @@ suite "Network errors - HTTP": privateAccess(JsonRpcProvider) privateAccess(RpcHttpClient) - let responseTimeout = proc(request: HttpRequestRef): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = + let responseTimeout = proc( + request: HttpRequestRef + ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = try: await sleepAsync(5.minutes) return await request.respond(Http200, "OK") @@ -186,3 +200,227 @@ suite "Network errors - HTTP": # We don't need to recreate each and every possible exception condition, as # they are all wrapped up in RpcPostError and converted to RpcNetworkError. # The tests above cover this conversion. + +# suite "Network errors - WebSocket": + +# var provider: JsonRpcProvider +# var mockWsServer: MockWebSocketServer +# var token: TestToken + +# setup: +# mockWsServer = MockWebSocketServer.init(initTAddress("127.0.0.1:0")) +# await mockWsServer.start() +# # Get the actual bound address +# let actualAddress = mockWsServer.localAddress() +# provider = JsonRpcProvider.new("ws://" & $actualAddress & "/ws") + +# let deployment = readDeployment() +# token = TestToken.new(!deployment.address(TestToken), provider) + +# teardown: +# await provider.close() +# await mockWsServer.stop() + +# proc registerRpcMethods(behavior: WebSocketBehavior) = +# mockWsServer.registerRpcBehavior("eth_accounts", behavior) +# mockWsServer.registerRpcBehavior("eth_call", behavior) +# mockWsServer.registerRpcBehavior("eth_sendTransaction", behavior) +# mockWsServer.registerRpcBehavior("eth_sendRawTransaction", behavior) +# mockWsServer.registerRpcBehavior("eth_newBlockFilter", behavior) +# mockWsServer.registerRpcBehavior("eth_newFilter", behavior) +# mockWsServer.registerRpcBehavior("eth_subscribe", behavior) + +# proc testCustomBehavior(errorName: string, behavior: WebSocketBehavior, errorType: type CatchableError) = +# let testNamePrefix = errorName & " behavior is converted to " & errorType.name & " for " + +# test testNamePrefix & "sending a manual RPC method request": +# registerRpcMethods(behavior) +# expect errorType: +# discard await provider.send("eth_accounts") + +# test testNamePrefix & "calling a provider method that converts errors": +# registerRpcMethods(behavior) +# expect errorType: +# discard await provider.listAccounts() + +# test testNamePrefix & "calling a view method of a contract": +# registerRpcMethods(behavior) +# expect errorType: +# discard await token.balanceOf(Address.example) + +# test testNamePrefix & "calling a contract method that executes a transaction": +# registerRpcMethods(behavior) +# expect errorType: +# token = TestToken.new(token.address, provider.getSigner()) +# discard await token.mint( +# Address.example, 100.u256, +# TransactionOverrides(gasLimit: 100.u256.some, chainId: 1.u256.some) +# ) + +# test testNamePrefix & "sending a manual transaction": +# registerRpcMethods(behavior) +# expect errorType: +# let tx = Transaction.example +# discard await provider.getSigner().sendTransaction(tx) + +# test testNamePrefix & "sending a raw transaction": +# registerRpcMethods(behavior) +# expect errorType: +# const pk_with_funds = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" +# let wallet = !Wallet.new(pk_with_funds) +# let tx = Transaction( +# to: wallet.address, +# nonce: some 0.u256, +# chainId: some 31337.u256, +# gasPrice: some 1_000_000_000.u256, +# gasLimit: some 21_000.u256, +# ) +# let signedTx = await wallet.signTransaction(tx) +# discard await provider.sendTransaction(signedTx) + +# test testNamePrefix & "subscribing to blocks": +# registerRpcMethods(behavior) +# expect errorType: +# let emptyHandler = proc(blckResult: ?!Block) = discard +# discard await provider.subscribe(emptyHandler) + +# test testNamePrefix & "subscribing to logs": +# registerRpcMethods(behavior) +# expect errorType: +# let filter = EventFilter(address: Address.example, topics: @[array[32, byte].example]) +# let emptyHandler = proc(log: ?!Log) = discard +# discard await provider.subscribe(filter, emptyHandler) + +# # WebSocket close codes equivalent to HTTP status codes +# testCustomBehavior( +# "Policy violation close (rate limit)", +# createBehavior(CloseWithCode, StatusPolicyError, "Policy violation - rate limited"), +# WebSocketPolicyError +# ) + +# testCustomBehavior( +# "Server error close", +# createBehavior(CloseWithCode, StatusUnexpectedError, "Internal server error"), +# JsonRpcProviderError +# ) + +# # testCustomBehavior( +# # "Service unavailable close", +# # createBehavior(CloseWithCode, StatusCodes.TryAgainLater, "Try again later"), +# # WebSocketServiceUnavailableError +# # ) + +# testCustomBehavior( +# "Abrupt disconnect", +# createBehavior(AbruptClose), +# RpcNetworkError +# ) + +# test "raises RpcNetworkError when WebSocket connection times out": +# registerRpcMethods(createBehavior(Timeout, delay = 5.minutes)) + +# # Set a short timeout on the WebSocket client +# privateAccess(JsonRpcProvider) +# privateAccess(RpcWebSocketClient) + +# let rpcClient = await provider.client +# let client = RpcWebSocketClient(rpcClient) +# # Note: Actual timeout setting depends on nim-websock implementation +# # This may need to be adjusted based on available APIs + +# expect RpcNetworkError: +# discard await provider.send("eth_accounts").wait(1.seconds) + +# test "raises RpcNetworkError when WebSocket connection is closed unexpectedly": +# # Start a request, then close the server +# let sendFuture = provider.send("eth_accounts") +# await sleepAsync(10.millis) +# await mockWsServer.stop() + +# expect RpcNetworkError: +# discard await sendFuture + +# test "raises RpcNetworkError when WebSocket connection fails to establish": +# # Stop the server first +# await mockWsServer.stop() + +# expect RpcNetworkError: +# let deadProvider = JsonRpcProvider.new("ws://127.0.0.1:9999/ws") +# discard await deadProvider.send("eth_accounts") + +# test "handles WebSocket protocol errors gracefully": +# registerRpcMethods(createBehavior(InvalidFrame)) + +# expect JsonRpcProviderError: # or whatever error nim-json-rpc maps protocol errors to +# discard await provider.send("eth_accounts") + +# test "handles oversized WebSocket messages": +# registerRpcMethods(createBehavior(MessageTooBig)) + +# expect RpcNetworkError: # Large message handling depends on client limits +# discard await provider.send("eth_accounts") + +# test "raises timeout error on slow WebSocket handshake": +# # Create a server that delays the WebSocket upgrade +# let slowServer = MockWebSocketServer.init(initTAddress("127.0.0.1:0")) +# # This would need custom implementation to delay handshake + +# expect WebSocketTimeoutError: +# let slowProvider = JsonRpcProvider.new("ws://127.0.0.1:9998/ws") +# discard await slowProvider.send("eth_accounts").wait(100.millis) + +# test "handles connection drops during message exchange": +# # Register normal behavior initially +# registerRpcMethods(createBehavior(Normal)) + +# # Start multiple requests +# let futures = @[ +# provider.send("eth_accounts"), +# provider.send("eth_call"), +# provider.send("eth_newBlockFilter") +# ] + +# # Close connections after a short delay +# await sleepAsync(5.millis) +# for conn in mockWsServer.connections: +# await conn.close(StatusCodes.AbnormalClosure, "Abnormal closure") + +# # All should fail with network error +# for future in futures: +# expect RpcNetworkError: +# discard await future + +# test "recovers from temporary WebSocket disconnections": +# # This test would verify client reconnection logic if implemented +# # Initial connection works +# registerRpcMethods(createBehavior(Normal)) +# let result1 = await provider.send("eth_accounts") +# check result1.isOk + +# # Simulate connection drop +# for conn in mockWsServer.connections: +# await conn.close(StatusCodes.GoingAway, "Going away") + +# # Depending on provider implementation, this might auto-reconnect +# # or need manual reconnection +# expect RpcNetworkError: +# discard await provider.send("eth_accounts") + +# test "handles WebSocket ping/pong timeouts": +# # This would test the ping/pong mechanism if the client supports it +# registerRpcMethods(createBehavior(Normal)) + +# # Mock a scenario where server doesn't respond to pings +# for conn in mockWsServer.connections: +# # Disable pong responses (if we had access to this) +# conn.onPing = nil + +# # This test would need to trigger ping timeout +# # The exact implementation depends on the websocket client capabilities + +# test "handles WebSocket close frame with invalid payload": +# # Test handling of malformed close frames +# registerRpcMethods(createBehavior(CloseWithCode, StatusCodes.ProtocolError, "")) + +# expect JsonRpcProviderError: +# discard await provider.send("eth_accounts")