Skip to content

Commit

Permalink
testing: Add a test for the grpc-web-proxy
Browse files Browse the repository at this point in the history
We create a standalone service and front it with the
grpc-web-proxy. Since the proxy must not rely on the payload to make
decisions we just implemented a simple test proto just for this case.
  • Loading branch information
cdecker committed Nov 7, 2024
1 parent 51589ee commit 50c4aa3
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 26 deletions.
3 changes: 2 additions & 1 deletion libs/gl-testing/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ testgrpc: ${REPO_ROOT}/libs/proto/glclient/scheduler.proto
mv ${TESTINGDIR}/gltesting/glclient/scheduler_grpc.py ${TESTINGDIR}/gltesting/scheduler_grpc.py
rm -rf ${TESTINGDIR}/gltesting/glclient


protoc:
uv run python3 -m grpc_tools.protoc -I. --python_out=. --pyi_out=. --purerpc_out=. --grpc_python_out=. gltesting/test.proto

93 changes: 68 additions & 25 deletions libs/gl-testing/gltesting/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,23 @@
from pathlib import Path
import logging
import sys
from pyln.testing.fixtures import bitcoind, teardown_checks, node_cls, test_name, executor, db_provider, test_base_dir, jsonschemas
from pyln.testing.fixtures import (
bitcoind,
teardown_checks,
node_cls,
test_name,
executor,
db_provider,
test_base_dir,
jsonschemas,
)
from gltesting.network import node_factory
from pyln.testing.fixtures import directory as str_directory
from decimal import Decimal
from gltesting.grpcweb import GrpcWebProxy
from clnvm import ClnVersionManager


logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
logging.getLogger().addHandler(logging.StreamHandler(sys.stdout))
logging.getLogger("sh").setLevel(logging.ERROR)
Expand All @@ -39,15 +50,15 @@ def paths():
# Should be a no-op after the first run
vm.get_all()

latest = [v for v in versions if 'gl' in v.tag][-1]
latest = [v for v in versions if "gl" in v.tag][-1]

os.environ['PATH'] += f":{vm.get_target_path(latest) / 'usr' / 'local' / 'bin'}"
os.environ["PATH"] += f":{vm.get_target_path(latest) / 'usr' / 'local' / 'bin'}"

yield
yield


@pytest.fixture()
def directory(str_directory : str) -> Path:
def directory(str_directory: str) -> Path:
return Path(str_directory) / "gl-testing"


Expand Down Expand Up @@ -105,31 +116,33 @@ def scheduler(scheduler_id, bitcoind):
btcproxy = bitcoind.get_proxy()

# Copied from pyln.testing.utils.NodeFactory.get_node
feerates=(15000, 11000, 7500, 3750)
feerates = (15000, 11000, 7500, 3750)

def mock_estimatesmartfee(r):
params = r['params']
if params == [2, 'CONSERVATIVE']:
params = r["params"]
if params == [2, "CONSERVATIVE"]:
feerate = feerates[0] * 4
elif params == [6, 'ECONOMICAL']:
elif params == [6, "ECONOMICAL"]:
feerate = feerates[1] * 4
elif params == [12, 'ECONOMICAL']:
elif params == [12, "ECONOMICAL"]:
feerate = feerates[2] * 4
elif params == [100, 'ECONOMICAL']:
elif params == [100, "ECONOMICAL"]:
feerate = feerates[3] * 4
else:
warnings.warn("Don't have a feerate set for {}/{}.".format(
params[0], params[1],
))
warnings.warn(
"Don't have a feerate set for {}/{}.".format(
params[0],
params[1],
)
)
feerate = 42
return {
'id': r['id'],
'error': None,
'result': {
'feerate': Decimal(feerate) / 10**8
},
"id": r["id"],
"error": None,
"result": {"feerate": Decimal(feerate) / 10**8},
}
btcproxy.mock_rpc('estimatesmartfee', mock_estimatesmartfee)

btcproxy.mock_rpc("estimatesmartfee", mock_estimatesmartfee)

s = Scheduler(bitcoind=btcproxy, grpc_port=grpc_port, identity=scheduler_id)
logger.debug(f"Scheduler is running at {s.grpc_addr}")
Expand All @@ -149,9 +162,7 @@ def mock_estimatesmartfee(r):
# here.

if s.debugger.reports != []:
raise ValueError(
f"Some signer reported an error: {s.debugger.reports}"
)
raise ValueError(f"Some signer reported an error: {s.debugger.reports}")


@pytest.fixture()
Expand All @@ -162,7 +173,7 @@ def clients(directory, scheduler, nobody_id):
yield clients


@pytest.fixture(scope='session', autouse=True)
@pytest.fixture(scope="session", autouse=True)
def cln_path() -> Path:
"""Ensure that the latest CLN version is in PATH.
Expand All @@ -175,5 +186,37 @@ def cln_path() -> Path:
"""
manager = ClnVersionManager()
v = manager.latest()
os.environ['PATH'] += f":{v.bin_path}"
os.environ["PATH"] += f":{v.bin_path}"
return v.bin_path


@pytest.fixture()
def grpc_test_server():
"""Creates a hello world server over grpc to test the web proxy against.
We explicitly do not use the real protos since the proxy must be
agnostic.
"""
import anyio
from threading import Thread
import purerpc
from util.grpcserver import Server

server = Server()
logging.getLogger("purerpc").setLevel(logging.DEBUG)
server.start()

yield server

server.stop()


@pytest.fixture()
def grpc_web_proxy(scheduler, grpc_test_server):
p = GrpcWebProxy(scheduler=scheduler, grpc_port=grpc_test_server.grpc_port)
p.start()

yield p

p.stop()
17 changes: 17 additions & 0 deletions libs/gl-testing/gltesting/test.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Just a small grpc definition to test the grpcweb implementation.

syntax = "proto3";

package gltesting;

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}
39 changes: 39 additions & 0 deletions libs/gl-testing/gltesting/test_grpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import purerpc
import gltesting.test_pb2 as gltesting_dot_test__pb2


class GreeterServicer(purerpc.Servicer):
async def SayHello(self, input_message):
raise NotImplementedError()

@property
def service(self) -> purerpc.Service:
service_obj = purerpc.Service(
"gltesting.Greeter"
)
service_obj.add_method(
"SayHello",
self.SayHello,
purerpc.RPCSignature(
purerpc.Cardinality.UNARY_UNARY,
gltesting_dot_test__pb2.HelloRequest,
gltesting_dot_test__pb2.HelloReply,
)
)
return service_obj


class GreeterStub:
def __init__(self, channel):
self._client = purerpc.Client(
"gltesting.Greeter",
channel
)
self.SayHello = self._client.get_method_stub(
"SayHello",
purerpc.RPCSignature(
purerpc.Cardinality.UNARY_UNARY,
gltesting_dot_test__pb2.HelloRequest,
gltesting_dot_test__pb2.HelloReply,
)
)
30 changes: 30 additions & 0 deletions libs/gl-testing/gltesting/test_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions libs/gl-testing/gltesting/test_pb2.pyi

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions libs/gl-testing/gltesting/test_pb2_grpc.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions libs/gl-testing/tests/test_grpc_web.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Tests that use a grpc-web client, without a client certificate, but
# payload signing for authentication.

from gltesting.fixtures import *
from gltesting.test_pb2_grpc import GreeterStub
from gltesting.test_pb2 import HelloRequest
import sonora.client

def test_start(grpc_web_proxy):
with sonora.client.insecure_web_channel(
f"http://localhost:{grpc_web_proxy.web_port}"
) as channel:
stub = GreeterStub(channel)
req = HelloRequest(name="greenlight")
print(stub.SayHello(req))

36 changes: 36 additions & 0 deletions libs/gl-testing/tests/util/grpcserver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# This is a simple grpc server serving the `gltesting/test.proto`
# protocol. It is used to test whether the grpc-web to grpc/h2
# proxying is working.

from gltesting.test_pb2 import HelloRequest, HelloReply
from gltesting.test_grpc import GreeterServicer
from ephemeral_port_reserve import reserve
import purerpc
from threading import Thread
import anyio



class Server(GreeterServicer):
def __init__(self, *args, **kwargs):
GreeterServicer.__init__(self, *args, **kwargs)
self.grpc_port = reserve()
self.inner = purerpc.Server(self.grpc_port)
self.thread: Thread | None = None
self.inner.add_service(self.service)

async def SayHello(self, message):
return HelloReply(message="Hello, " + message.name)

def start(self):
def target():
try:
anyio.run(self.inner.serve_async)
except Exception as e:
print("Error starting the grpc backend")

self.thread = Thread(target=target, daemon=True)
self.thread.start()

def stop(self):
self.inner.aclose

0 comments on commit 50c4aa3

Please sign in to comment.