diff --git a/libs/gl-testing/Makefile b/libs/gl-testing/Makefile index 8c16e3f78..1f6b6d5d5 100644 --- a/libs/gl-testing/Makefile +++ b/libs/gl-testing/Makefile @@ -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 diff --git a/libs/gl-testing/gltesting/fixtures.py b/libs/gl-testing/gltesting/fixtures.py index 3347e6a9a..36ffce1d1 100644 --- a/libs/gl-testing/gltesting/fixtures.py +++ b/libs/gl-testing/gltesting/fixtures.py @@ -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) @@ -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" @@ -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}") @@ -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() @@ -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. @@ -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() diff --git a/libs/gl-testing/gltesting/test.proto b/libs/gl-testing/gltesting/test.proto new file mode 100644 index 000000000..c2f00bc86 --- /dev/null +++ b/libs/gl-testing/gltesting/test.proto @@ -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; +} diff --git a/libs/gl-testing/gltesting/test_grpc.py b/libs/gl-testing/gltesting/test_grpc.py new file mode 100644 index 000000000..e2b09c3df --- /dev/null +++ b/libs/gl-testing/gltesting/test_grpc.py @@ -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, + ) + ) \ No newline at end of file diff --git a/libs/gl-testing/gltesting/test_pb2.py b/libs/gl-testing/gltesting/test_pb2.py new file mode 100644 index 000000000..efa688191 --- /dev/null +++ b/libs/gl-testing/gltesting/test_pb2.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: gltesting/test.proto +# Protobuf Python Version: 4.25.1 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x14gltesting/test.proto\x12\tgltesting\"\x1c\n\x0cHelloRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x1d\n\nHelloReply\x12\x0f\n\x07message\x18\x01 \x01(\t2E\n\x07Greeter\x12:\n\x08SayHello\x12\x17.gltesting.HelloRequest\x1a\x15.gltesting.HelloReplyb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'gltesting.test_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_HELLOREQUEST']._serialized_start=35 + _globals['_HELLOREQUEST']._serialized_end=63 + _globals['_HELLOREPLY']._serialized_start=65 + _globals['_HELLOREPLY']._serialized_end=94 + _globals['_GREETER']._serialized_start=96 + _globals['_GREETER']._serialized_end=165 +# @@protoc_insertion_point(module_scope) diff --git a/libs/gl-testing/gltesting/test_pb2.pyi b/libs/gl-testing/gltesting/test_pb2.pyi new file mode 100644 index 000000000..bf0bd395a --- /dev/null +++ b/libs/gl-testing/gltesting/test_pb2.pyi @@ -0,0 +1,17 @@ +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Optional as _Optional + +DESCRIPTOR: _descriptor.FileDescriptor + +class HelloRequest(_message.Message): + __slots__ = ("name",) + NAME_FIELD_NUMBER: _ClassVar[int] + name: str + def __init__(self, name: _Optional[str] = ...) -> None: ... + +class HelloReply(_message.Message): + __slots__ = ("message",) + MESSAGE_FIELD_NUMBER: _ClassVar[int] + message: str + def __init__(self, message: _Optional[str] = ...) -> None: ... diff --git a/libs/gl-testing/gltesting/test_pb2_grpc.py b/libs/gl-testing/gltesting/test_pb2_grpc.py new file mode 100644 index 000000000..395457cd5 --- /dev/null +++ b/libs/gl-testing/gltesting/test_pb2_grpc.py @@ -0,0 +1,66 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from gltesting import test_pb2 as gltesting_dot_test__pb2 + + +class GreeterStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.SayHello = channel.unary_unary( + '/gltesting.Greeter/SayHello', + request_serializer=gltesting_dot_test__pb2.HelloRequest.SerializeToString, + response_deserializer=gltesting_dot_test__pb2.HelloReply.FromString, + ) + + +class GreeterServicer(object): + """Missing associated documentation comment in .proto file.""" + + def SayHello(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_GreeterServicer_to_server(servicer, server): + rpc_method_handlers = { + 'SayHello': grpc.unary_unary_rpc_method_handler( + servicer.SayHello, + request_deserializer=gltesting_dot_test__pb2.HelloRequest.FromString, + response_serializer=gltesting_dot_test__pb2.HelloReply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'gltesting.Greeter', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class Greeter(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def SayHello(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/gltesting.Greeter/SayHello', + gltesting_dot_test__pb2.HelloRequest.SerializeToString, + gltesting_dot_test__pb2.HelloReply.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/libs/gl-testing/tests/test_grpc_web.py b/libs/gl-testing/tests/test_grpc_web.py new file mode 100644 index 000000000..6feca29d5 --- /dev/null +++ b/libs/gl-testing/tests/test_grpc_web.py @@ -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)) + diff --git a/libs/gl-testing/tests/util/grpcserver.py b/libs/gl-testing/tests/util/grpcserver.py new file mode 100644 index 000000000..901ac6e05 --- /dev/null +++ b/libs/gl-testing/tests/util/grpcserver.py @@ -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