@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2021 EmeraldPay, Inc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.emeraldpay.dshackle.proxy
|
||||
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp
|
||||
import io.emeraldpay.dshackle.rpc.NativeCall
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import org.jetbrains.annotations.NotNull
|
||||
import reactor.core.publisher.Flux
|
||||
import reactor.core.publisher.Mono
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
class BaseHandlerSpec extends Specification {
|
||||
|
||||
def requestHandler = new AccessHandlerHttp.NoOpHandler()
|
||||
|
||||
def "Return empty for empty single call"() {
|
||||
setup:
|
||||
def handler = new BaseHandlerImpl(new WriteRpcJson(), Stub(NativeCall), Stub(ProxyServer.RequestMetricsFactory))
|
||||
when:
|
||||
def act = Mono.from(handler.execute(Chain.ETHEREUM, new ProxyCall(ProxyCall.RpcType.SINGLE), requestHandler))
|
||||
.block(Duration.ofSeconds(1))
|
||||
then:
|
||||
act == ""
|
||||
}
|
||||
|
||||
def "Return empty array for empty batch call"() {
|
||||
setup:
|
||||
def handler = new BaseHandlerImpl(new WriteRpcJson(), Stub(NativeCall), Stub(ProxyServer.RequestMetricsFactory))
|
||||
when:
|
||||
def act = Mono.from(handler.execute(Chain.ETHEREUM, new ProxyCall(ProxyCall.RpcType.BATCH), requestHandler))
|
||||
.block(Duration.ofSeconds(1))
|
||||
then:
|
||||
act == "[]"
|
||||
}
|
||||
|
||||
def "Execute single call"() {
|
||||
setup:
|
||||
def nativeCall = Mock(NativeCall)
|
||||
def handler = new BaseHandlerImpl(new WriteRpcJson(), nativeCall, Stub(ProxyServer.RequestMetricsFactory))
|
||||
|
||||
def request = BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
.setMethod("eth_test")
|
||||
.setId(0)
|
||||
.build()
|
||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||
call.items.add(request)
|
||||
call.ids[0] = 5
|
||||
def response = new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
|
||||
when:
|
||||
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler))
|
||||
.collectList()
|
||||
.block(Duration.ofSeconds(1))
|
||||
.join("")
|
||||
then:
|
||||
act == '{"jsonrpc":"2.0","id":5,"result":{"foo": 1}}'
|
||||
1 * nativeCall.nativeCallResult(_) >> Flux.fromIterable([response])
|
||||
}
|
||||
|
||||
def "Execute batch call with one item"() {
|
||||
setup:
|
||||
def nativeCall = Mock(NativeCall)
|
||||
def handler = new BaseHandlerImpl(new WriteRpcJson(), nativeCall, Stub(ProxyServer.RequestMetricsFactory))
|
||||
|
||||
def request = BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
.setMethod("eth_test")
|
||||
.setId(0)
|
||||
.build()
|
||||
def call = new ProxyCall(ProxyCall.RpcType.BATCH)
|
||||
call.items.add(request)
|
||||
call.ids[0] = 5
|
||||
def response = new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
|
||||
when:
|
||||
def act = Flux.from(handler.execute(Chain.ETHEREUM, call, requestHandler))
|
||||
.collectList()
|
||||
.block(Duration.ofSeconds(1))
|
||||
.join("")
|
||||
then:
|
||||
act == '[{"jsonrpc":"2.0","id":5,"result":{"foo": 1}}]'
|
||||
1 * nativeCall.nativeCallResult(_) >> Flux.fromIterable([response])
|
||||
}
|
||||
|
||||
class BaseHandlerImpl extends BaseHandler {
|
||||
|
||||
BaseHandlerImpl(@NotNull WriteRpcJson writeRpcJson, @NotNull NativeCall nativeCall, @NotNull ProxyServer.RequestMetricsFactory requestMetrics) {
|
||||
super(writeRpcJson, nativeCall, requestMetrics)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020 ETCDEV GmbH
|
||||
* Copyright (c) 2020 EmeraldPay, Inc
|
||||
* Copyright (c) 2021 EmeraldPay, Inc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,11 +18,8 @@ package io.emeraldpay.dshackle.proxy
|
||||
import com.google.protobuf.ByteString
|
||||
import io.emeraldpay.api.proto.BlockchainOuterClass
|
||||
import io.emeraldpay.api.proto.Common
|
||||
import io.emeraldpay.dshackle.TlsSetup
|
||||
import io.emeraldpay.dshackle.config.ProxyConfig
|
||||
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp
|
||||
import io.emeraldpay.dshackle.rpc.NativeCall
|
||||
import io.emeraldpay.dshackle.test.TestingCommons
|
||||
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
|
||||
import io.emeraldpay.etherjar.rpc.RpcException
|
||||
import io.emeraldpay.grpc.Chain
|
||||
@@ -35,64 +31,7 @@ import spock.lang.Specification
|
||||
import java.time.Duration
|
||||
import java.util.function.Function
|
||||
|
||||
class ProxyServerSpec extends Specification {
|
||||
|
||||
def "Uses NativeCall"() {
|
||||
setup:
|
||||
NativeCall nativeCall = Mock(NativeCall)
|
||||
def predefined = { a -> Flux.just("hello") } as Function
|
||||
|
||||
WriteRpcJson writeRpcJson = Mock {
|
||||
1 * toJsons(_) >> predefined
|
||||
}
|
||||
|
||||
ProxyServer server = new ProxyServer(
|
||||
new ProxyConfig(),
|
||||
new ReadRpcJson(),
|
||||
writeRpcJson,
|
||||
nativeCall,
|
||||
new TlsSetup(TestingCommons.fileResolver()),
|
||||
new AccessHandlerHttp.NoOpFactory()
|
||||
)
|
||||
|
||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||
call.ids[1] = 1
|
||||
call.items.add(
|
||||
BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
.setMethod("eth_hello")
|
||||
.build()
|
||||
)
|
||||
when:
|
||||
def act = server.execute(Chain.ETHEREUM, call, new AccessHandlerHttp.NoOpHandler())
|
||||
|
||||
then:
|
||||
1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, "".bytes, null))
|
||||
StepVerifier.create(act)
|
||||
.expectNext("hello")
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
def "Return error on invalid request"() {
|
||||
setup:
|
||||
ReadRpcJson read = Mock(ReadRpcJson) {
|
||||
1 * apply(_) >> { throw new RpcException(-32123, "test", new JsonRpcResponse.NumberId(4)) }
|
||||
}
|
||||
def server = new ProxyServer(
|
||||
Stub(ProxyConfig),
|
||||
read,
|
||||
Stub(WriteRpcJson), Stub(NativeCall), Stub(TlsSetup),
|
||||
new AccessHandlerHttp.NoOpFactory()
|
||||
)
|
||||
when:
|
||||
def act = server.processRequest(Chain.ETHEREUM, Mono.just("".bytes), new AccessHandlerHttp.NoOpHandler())
|
||||
.map { new String(it.array()) }
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
.expectNext('{"jsonrpc":"2.0","id":4,"error":{"code":-32123,"message":"test"}}')
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
class HttpHandlerSpec extends Specification {
|
||||
|
||||
def "Calls access log handler"() {
|
||||
setup:
|
||||
@@ -107,30 +46,78 @@ class ProxyServerSpec extends Specification {
|
||||
.addItems(reqItem)
|
||||
.build()
|
||||
|
||||
|
||||
ReadRpcJson read = Mock(ReadRpcJson) {
|
||||
1 * apply(_) >> new ProxyCall(ProxyCall.RpcType.SINGLE).tap { it.items.add(reqItem) }
|
||||
}
|
||||
NativeCall nativeCall = Mock(NativeCall) {
|
||||
1 * nativeCallResult(_) >> Flux.fromIterable([respItem])
|
||||
}
|
||||
def handler = Mock(AccessHandlerHttp.RequestHandler.class)
|
||||
|
||||
def server = new ProxyServer(
|
||||
Stub(ProxyConfig),
|
||||
read,
|
||||
new WriteRpcJson(),
|
||||
nativeCall,
|
||||
Stub(TlsSetup),
|
||||
new AccessHandlerHttp.NoOpFactory()
|
||||
def accessHandler = Mock(AccessHandlerHttp.RequestHandler)
|
||||
def accessHandlerFactory = Mock(AccessHandlerHttp.HandlerFactory) {
|
||||
_ * it.create(_,) >> accessHandler
|
||||
}
|
||||
def handler = new HttpHandler(
|
||||
new ReadRpcJson(), new WriteRpcJson(),
|
||||
nativeCall, accessHandlerFactory, Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
|
||||
when:
|
||||
server.processRequest(Chain.ETHEREUM, Mono.just("".bytes), handler)
|
||||
handler.execute(Chain.ETHEREUM, [reqItem], accessHandler)
|
||||
.blockLast()
|
||||
|
||||
then:
|
||||
1 * handler.onRequest(req)
|
||||
1 * handler.onResponse(respItem)
|
||||
1 * accessHandler.onRequest(req)
|
||||
1 * accessHandler.onResponse(respItem)
|
||||
}
|
||||
|
||||
def "Return error on invalid request"() {
|
||||
setup:
|
||||
ReadRpcJson read = Mock(ReadRpcJson) {
|
||||
1 * apply(_) >> { throw new RpcException(-32123, "test", new JsonRpcResponse.NumberId(4)) }
|
||||
}
|
||||
|
||||
def handler = new HttpHandler(
|
||||
read, new WriteRpcJson(),
|
||||
Stub(NativeCall), Stub(AccessHandlerHttp.HandlerFactory), Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
when:
|
||||
|
||||
def act = handler.processRequest(Chain.ETHEREUM, Mono.just("".bytes), new AccessHandlerHttp.NoOpHandler())
|
||||
.map { new String(it.array()) }
|
||||
then:
|
||||
StepVerifier.create(act)
|
||||
.expectNext('{"jsonrpc":"2.0","id":4,"error":{"code":-32123,"message":"test"}}')
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
|
||||
def "Uses NativeCall"() {
|
||||
setup:
|
||||
NativeCall nativeCall = Mock(NativeCall)
|
||||
def predefined = { a -> Flux.just("hello") } as Function
|
||||
|
||||
WriteRpcJson writeRpcJson = Mock {
|
||||
1 * toJsons(_) >> predefined
|
||||
}
|
||||
|
||||
|
||||
def handler = new HttpHandler(
|
||||
new ReadRpcJson(), writeRpcJson,
|
||||
nativeCall, Stub(AccessHandlerHttp.HandlerFactory), Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
|
||||
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
|
||||
call.ids[1] = 1
|
||||
call.items.add(
|
||||
BlockchainOuterClass.NativeCallItem.newBuilder()
|
||||
.setMethod("eth_hello")
|
||||
.build()
|
||||
)
|
||||
when:
|
||||
def act = handler.execute(Chain.ETHEREUM, call, new AccessHandlerHttp.NoOpHandler())
|
||||
|
||||
then:
|
||||
1 * nativeCall.nativeCallResult(_) >> Flux.just(new NativeCall.CallResult(1, "".bytes, null))
|
||||
StepVerifier.create(act)
|
||||
.expectNext("hello")
|
||||
.expectComplete()
|
||||
.verify(Duration.ofSeconds(1))
|
||||
}
|
||||
}
|
||||
@@ -224,4 +224,12 @@ class ReadRpcJsonSpec extends Specification {
|
||||
t.rpcMessage.toLowerCase() == "params must be an array"
|
||||
t.details == new JsonRpcResponse.NumberId(2)
|
||||
}
|
||||
|
||||
def "Error if json is broken"() {
|
||||
when:
|
||||
reader.apply('{"id":2, "method":"net_peerCount", "params"'.bytes)
|
||||
then:
|
||||
def t = thrown(RpcException)
|
||||
t.code == -32700
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright (c) 2021 EmeraldPay, Inc
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package io.emeraldpay.dshackle.proxy
|
||||
|
||||
import io.emeraldpay.dshackle.monitoring.accesslog.AccessHandlerHttp
|
||||
import io.emeraldpay.dshackle.rpc.NativeCall
|
||||
import io.emeraldpay.dshackle.rpc.NativeSubscribe
|
||||
import io.emeraldpay.etherjar.rpc.json.RequestJson
|
||||
import io.emeraldpay.grpc.Chain
|
||||
import reactor.core.publisher.Flux
|
||||
import spock.lang.Specification
|
||||
|
||||
import java.time.Duration
|
||||
|
||||
class WebsocketHandlerSpec extends Specification {
|
||||
|
||||
def requestHandler = new AccessHandlerHttp.NoOpHandler()
|
||||
|
||||
def "Parse standard RPC request"() {
|
||||
setup:
|
||||
def handler = new WebsocketHandler(
|
||||
new ReadRpcJson(), Stub(WriteRpcJson), Stub(NativeCall), Stub(NativeSubscribe), Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
when:
|
||||
def act = handler.parseRequest('{"id": 5, "jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": ["0x100001", false]}'.bytes)
|
||||
.block(Duration.ofSeconds(1))
|
||||
|
||||
then:
|
||||
act.id == 5
|
||||
act.method == "eth_getBlockByNumber"
|
||||
act.params == ["0x100001", false]
|
||||
}
|
||||
|
||||
def "Parse to empty an invalid request"() {
|
||||
setup:
|
||||
def handler = new WebsocketHandler(
|
||||
new ReadRpcJson(), Stub(WriteRpcJson), Stub(NativeCall), Stub(NativeSubscribe), Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
when:
|
||||
def act = handler.parseRequest('hello world'.bytes)
|
||||
.block(Duration.ofSeconds(1))
|
||||
|
||||
then:
|
||||
act == null
|
||||
}
|
||||
|
||||
def "Parse to empty a batch request"() {
|
||||
setup:
|
||||
def req1 = '{"id": 5, "jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": ["0x100001", false]}'
|
||||
def handler = new WebsocketHandler(
|
||||
new ReadRpcJson(), Stub(WriteRpcJson), Stub(NativeCall), Stub(NativeSubscribe), Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
when:
|
||||
def act = handler.parseRequest("[$req1]".bytes)
|
||||
.block(Duration.ofSeconds(1))
|
||||
|
||||
then:
|
||||
act == null
|
||||
}
|
||||
|
||||
def "Respond to a single call"() {
|
||||
setup:
|
||||
def response = new NativeCall.CallResult(0, '{"foo": 1}'.bytes, null)
|
||||
|
||||
def nativeCall = Mock(NativeCall) {
|
||||
1 * it.nativeCallResult(_) >> Flux.fromIterable([response])
|
||||
}
|
||||
def handler = new WebsocketHandler(
|
||||
new ReadRpcJson(), new WriteRpcJson(), nativeCall, Stub(NativeSubscribe), Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
|
||||
def request = new RequestJson("foo_test", [], 2)
|
||||
when:
|
||||
def act = handler.respond(Chain.ETHEREUM, Flux.just(request), requestHandler)
|
||||
.single()
|
||||
.block(Duration.ofSeconds(1))
|
||||
then:
|
||||
act == '{"jsonrpc":"2.0","id":2,"result":{"foo": 1}}'
|
||||
}
|
||||
|
||||
def "Respond to a subscription call"() {
|
||||
setup:
|
||||
def response1 = [foo: 1]
|
||||
def response2 = [foo: 2]
|
||||
|
||||
def nativeSubscribe = Mock(NativeSubscribe) {
|
||||
1 * it.subscribe(Chain.ETHEREUM, "foo_test", null) >> Flux.fromIterable([response1, response2])
|
||||
}
|
||||
def handler = new WebsocketHandler(
|
||||
new ReadRpcJson(), new WriteRpcJson(), Stub(NativeCall), nativeSubscribe, Stub(ProxyServer.RequestMetricsFactory)
|
||||
)
|
||||
|
||||
def request = new RequestJson("eth_subscribe", ["foo_test"], 2)
|
||||
when:
|
||||
def act = handler.respond(Chain.ETHEREUM, Flux.just(request), requestHandler)
|
||||
.collectList()
|
||||
.block(Duration.ofSeconds(1))
|
||||
then:
|
||||
act[0] == '{"jsonrpc":"2.0","id":2,"result":"0000000000000001"}'
|
||||
act[1] == '{"jsonrpc":"2.0","method":"eth_subscription","params":{"result":{"foo":1},"subscription":"0000000000000001"}}'
|
||||
act[2] == '{"jsonrpc":"2.0","method":"eth_subscription","params":{"result":{"foo":2},"subscription":"0000000000000001"}}'
|
||||
act.size() == 3
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user