Skip to content

Commit fddbfe9

Browse files
committed
testing: Add a test for the grpc-web-proxy
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.
1 parent ea063d1 commit fddbfe9

File tree

9 files changed

+291
-26
lines changed

9 files changed

+291
-26
lines changed

libs/gl-testing/Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@ testgrpc: ${REPO_ROOT}/libs/proto/glclient/scheduler.proto
2727
mv ${TESTINGDIR}/gltesting/glclient/scheduler_grpc.py ${TESTINGDIR}/gltesting/scheduler_grpc.py
2828
rm -rf ${TESTINGDIR}/gltesting/glclient
2929

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

libs/gl-testing/gltesting/fixtures.py

Lines changed: 68 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,23 @@
1010
from pathlib import Path
1111
import logging
1212
import sys
13-
from pyln.testing.fixtures import bitcoind, teardown_checks, node_cls, test_name, executor, db_provider, test_base_dir, jsonschemas
13+
from pyln.testing.fixtures import (
14+
bitcoind,
15+
teardown_checks,
16+
node_cls,
17+
test_name,
18+
executor,
19+
db_provider,
20+
test_base_dir,
21+
jsonschemas,
22+
)
1423
from gltesting.network import node_factory
1524
from pyln.testing.fixtures import directory as str_directory
1625
from decimal import Decimal
26+
from gltesting.grpcweb import GrpcWebProxy
1727
from clnvm import ClnVersionManager
1828

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

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

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

46-
yield
57+
yield
4758

4859

4960
@pytest.fixture()
50-
def directory(str_directory : str) -> Path:
61+
def directory(str_directory: str) -> Path:
5162
return Path(str_directory) / "gl-testing"
5263

5364

@@ -105,31 +116,33 @@ def scheduler(scheduler_id, bitcoind):
105116
btcproxy = bitcoind.get_proxy()
106117

107118
# Copied from pyln.testing.utils.NodeFactory.get_node
108-
feerates=(15000, 11000, 7500, 3750)
119+
feerates = (15000, 11000, 7500, 3750)
109120

110121
def mock_estimatesmartfee(r):
111-
params = r['params']
112-
if params == [2, 'CONSERVATIVE']:
122+
params = r["params"]
123+
if params == [2, "CONSERVATIVE"]:
113124
feerate = feerates[0] * 4
114-
elif params == [6, 'ECONOMICAL']:
125+
elif params == [6, "ECONOMICAL"]:
115126
feerate = feerates[1] * 4
116-
elif params == [12, 'ECONOMICAL']:
127+
elif params == [12, "ECONOMICAL"]:
117128
feerate = feerates[2] * 4
118-
elif params == [100, 'ECONOMICAL']:
129+
elif params == [100, "ECONOMICAL"]:
119130
feerate = feerates[3] * 4
120131
else:
121-
warnings.warn("Don't have a feerate set for {}/{}.".format(
122-
params[0], params[1],
123-
))
132+
warnings.warn(
133+
"Don't have a feerate set for {}/{}.".format(
134+
params[0],
135+
params[1],
136+
)
137+
)
124138
feerate = 42
125139
return {
126-
'id': r['id'],
127-
'error': None,
128-
'result': {
129-
'feerate': Decimal(feerate) / 10**8
130-
},
140+
"id": r["id"],
141+
"error": None,
142+
"result": {"feerate": Decimal(feerate) / 10**8},
131143
}
132-
btcproxy.mock_rpc('estimatesmartfee', mock_estimatesmartfee)
144+
145+
btcproxy.mock_rpc("estimatesmartfee", mock_estimatesmartfee)
133146

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

151164
if s.debugger.reports != []:
152-
raise ValueError(
153-
f"Some signer reported an error: {s.debugger.reports}"
154-
)
165+
raise ValueError(f"Some signer reported an error: {s.debugger.reports}")
155166

156167

157168
@pytest.fixture()
@@ -162,7 +173,7 @@ def clients(directory, scheduler, nobody_id):
162173
yield clients
163174

164175

165-
@pytest.fixture(scope='session', autouse=True)
176+
@pytest.fixture(scope="session", autouse=True)
166177
def cln_path() -> Path:
167178
"""Ensure that the latest CLN version is in PATH.
168179
@@ -175,5 +186,37 @@ def cln_path() -> Path:
175186
"""
176187
manager = ClnVersionManager()
177188
v = manager.latest()
178-
os.environ['PATH'] += f":{v.bin_path}"
189+
os.environ["PATH"] += f":{v.bin_path}"
179190
return v.bin_path
191+
192+
193+
@pytest.fixture()
194+
def grpc_test_server():
195+
"""Creates a hello world server over grpc to test the web proxy against.
196+
197+
We explicitly do not use the real protos since the proxy must be
198+
agnostic.
199+
200+
"""
201+
import anyio
202+
from threading import Thread
203+
import purerpc
204+
from util.grpcserver import Server
205+
206+
server = Server()
207+
logging.getLogger("purerpc").setLevel(logging.DEBUG)
208+
server.start()
209+
210+
yield server
211+
212+
server.stop()
213+
214+
215+
@pytest.fixture()
216+
def grpc_web_proxy(scheduler, grpc_test_server):
217+
p = GrpcWebProxy(scheduler=scheduler, grpc_port=grpc_test_server.grpc_port)
218+
p.start()
219+
220+
yield p
221+
222+
p.stop()
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Just a small grpc definition to test the grpcweb implementation.
2+
3+
syntax = "proto3";
4+
5+
package gltesting;
6+
7+
service Greeter {
8+
rpc SayHello (HelloRequest) returns (HelloReply);
9+
}
10+
11+
message HelloRequest {
12+
string name = 1;
13+
}
14+
15+
message HelloReply {
16+
string message = 1;
17+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import purerpc
2+
import gltesting.test_pb2 as gltesting_dot_test__pb2
3+
4+
5+
class GreeterServicer(purerpc.Servicer):
6+
async def SayHello(self, input_message):
7+
raise NotImplementedError()
8+
9+
@property
10+
def service(self) -> purerpc.Service:
11+
service_obj = purerpc.Service(
12+
"gltesting.Greeter"
13+
)
14+
service_obj.add_method(
15+
"SayHello",
16+
self.SayHello,
17+
purerpc.RPCSignature(
18+
purerpc.Cardinality.UNARY_UNARY,
19+
gltesting_dot_test__pb2.HelloRequest,
20+
gltesting_dot_test__pb2.HelloReply,
21+
)
22+
)
23+
return service_obj
24+
25+
26+
class GreeterStub:
27+
def __init__(self, channel):
28+
self._client = purerpc.Client(
29+
"gltesting.Greeter",
30+
channel
31+
)
32+
self.SayHello = self._client.get_method_stub(
33+
"SayHello",
34+
purerpc.RPCSignature(
35+
purerpc.Cardinality.UNARY_UNARY,
36+
gltesting_dot_test__pb2.HelloRequest,
37+
gltesting_dot_test__pb2.HelloReply,
38+
)
39+
)

libs/gl-testing/gltesting/test_pb2.py

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/gl-testing/gltesting/test_pb2.pyi

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

libs/gl-testing/gltesting/test_pb2_grpc.py

Lines changed: 66 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Tests that use a grpc-web client, without a client certificate, but
2+
# payload signing for authentication.
3+
4+
from gltesting.fixtures import *
5+
from gltesting.test_pb2_grpc import GreeterStub
6+
from gltesting.test_pb2 import HelloRequest
7+
import sonora.client
8+
9+
def test_start(grpc_web_proxy):
10+
with sonora.client.insecure_web_channel(
11+
f"http://localhost:{grpc_web_proxy.web_port}"
12+
) as channel:
13+
stub = GreeterStub(channel)
14+
req = HelloRequest(name="greenlight")
15+
print(stub.SayHello(req))
16+
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# This is a simple grpc server serving the `gltesting/test.proto`
2+
# protocol. It is used to test whether the grpc-web to grpc/h2
3+
# proxying is working.
4+
5+
from gltesting.test_pb2 import HelloRequest, HelloReply
6+
from gltesting.test_grpc import GreeterServicer
7+
from ephemeral_port_reserve import reserve
8+
import purerpc
9+
from threading import Thread
10+
import anyio
11+
12+
13+
14+
class Server(GreeterServicer):
15+
def __init__(self, *args, **kwargs):
16+
GreeterServicer.__init__(self, *args, **kwargs)
17+
self.grpc_port = reserve()
18+
self.inner = purerpc.Server(self.grpc_port)
19+
self.thread: Thread | None = None
20+
self.inner.add_service(self.service)
21+
22+
async def SayHello(self, message):
23+
return HelloReply(message="Hello, " + message.name)
24+
25+
def start(self):
26+
def target():
27+
try:
28+
anyio.run(self.inner.serve_async)
29+
except Exception as e:
30+
print("Error starting the grpc backend")
31+
32+
self.thread = Thread(target=target, daemon=True)
33+
self.thread.start()
34+
35+
def stop(self):
36+
self.inner.aclose

0 commit comments

Comments
 (0)