|
9 | 9 | """ |
10 | 10 |
|
11 | 11 | import socket |
12 | | -import pytest |
| 12 | +import threading |
| 13 | +import time |
13 | 14 |
|
| 15 | +import pytest |
14 | 16 | from hyperframe.frame import Frame |
15 | | -from werkzeug.datastructures import Headers |
16 | | -from localstack_extensions.utils.docker import ProxiedDockerContainerExtension |
| 17 | +from localstack.utils.net import get_free_tcp_port |
| 18 | +from rolo import Router |
| 19 | +from rolo.gateway import Gateway |
| 20 | +from twisted.internet import reactor |
| 21 | +from twisted.web import server as twisted_server |
17 | 22 |
|
| 23 | +from localstack_extensions.utils.docker import ProxiedDockerContainerExtension |
18 | 24 |
|
19 | 25 | GRPCBIN_IMAGE = "moul/grpcbin" |
20 | 26 | GRPCBIN_INSECURE_PORT = 9000 # HTTP/2 without TLS |
@@ -53,51 +59,75 @@ def _tcp_health_check(): |
53 | 59 | image_name=GRPCBIN_IMAGE, |
54 | 60 | container_ports=[GRPCBIN_INSECURE_PORT, GRPCBIN_SECURE_PORT], |
55 | 61 | health_check_fn=_tcp_health_check, |
| 62 | + tcp_ports=[GRPCBIN_INSECURE_PORT], # Enable raw TCP proxying for gRPC/HTTP2 |
56 | 63 | ) |
57 | 64 |
|
58 | | - def http2_request_matcher(self, headers: Headers) -> bool: |
59 | | - """ |
60 | | - gRPC services use direct TCP connections, not HTTP gateway routing. |
61 | | - This method is not used in these tests but is required by the base class. |
62 | | - """ |
63 | | - return False |
| 65 | + def tcp_connection_matcher(self, data: bytes) -> bool: |
| 66 | + """Detect HTTP/2 connection preface to route gRPC/HTTP2 traffic.""" |
| 67 | + # HTTP/2 connections start with the connection preface |
| 68 | + if len(data) >= len(HTTP2_PREFACE): |
| 69 | + return data.startswith(HTTP2_PREFACE) |
| 70 | + # Also match if we have partial preface data (for early detection) |
| 71 | + return len(data) > 0 and HTTP2_PREFACE.startswith(data) |
64 | 72 |
|
65 | 73 |
|
66 | 74 | @pytest.fixture(scope="session") |
67 | | -def grpcbin_extension(): |
| 75 | +def grpcbin_extension_server(): |
68 | 76 | """ |
69 | | - Start grpcbin using ProxiedDockerContainerExtension. |
| 77 | + Start grpcbin using ProxiedDockerContainerExtension with a test gateway server. |
70 | 78 |
|
71 | | - This tests the Docker container management capabilities while providing |
72 | | - a realistic gRPC/HTTP2 test service for integration tests. |
| 79 | + This tests the Docker container management and proxy capabilities by: |
| 80 | + 1. Starting the grpcbin container via the extension |
| 81 | + 2. Setting up a Gateway with the extension's routes and TCP patches |
| 82 | + 3. Serving the Gateway on a test port via Twisted |
| 83 | + 4. Returning server info for end-to-end testing |
73 | 84 | """ |
74 | 85 | extension = GrpcbinExtension() |
75 | 86 |
|
76 | | - # Start the container using the extension infrastructure |
77 | | - extension.start_container() |
| 87 | + # Create router and update with extension routes |
| 88 | + # This will start the grpcbin container and apply TCP protocol patches |
| 89 | + router = Router() |
| 90 | + extension.update_gateway_routes(router) |
78 | 91 |
|
79 | | - yield extension |
| 92 | + # Create a Gateway with proper TCP support |
| 93 | + # The TCP patches are applied by update_gateway_routes above |
| 94 | + gateway = Gateway(router) |
80 | 95 |
|
81 | | - # Cleanup |
82 | | - extension.on_platform_shutdown() |
| 96 | + # Start gateway on a test port using Twisted |
| 97 | + test_port = get_free_tcp_port() |
| 98 | + site = twisted_server.Site(gateway) |
| 99 | + listener = reactor.listenTCP(test_port, site) |
83 | 100 |
|
| 101 | + # Run reactor in background thread |
| 102 | + def run_reactor(): |
| 103 | + reactor.run(installSignalHandlers=False) |
84 | 104 |
|
85 | | -@pytest.fixture |
86 | | -def grpcbin_host(grpcbin_extension): |
87 | | - """Return the host address for the grpcbin container.""" |
88 | | - return grpcbin_extension.container_host |
| 105 | + reactor_thread = threading.Thread(target=run_reactor, daemon=True) |
| 106 | + reactor_thread.start() |
89 | 107 |
|
| 108 | + # Wait for reactor to start - not ideal, but should work as a simple solution |
| 109 | + time.sleep(0.5) |
90 | 110 |
|
91 | | -@pytest.fixture |
92 | | -def grpcbin_insecure_port(grpcbin_extension): |
93 | | - """Return the insecure (HTTP/2 without TLS) port for grpcbin.""" |
94 | | - return GRPCBIN_INSECURE_PORT |
| 111 | + # Return server information for tests |
| 112 | + server_info = { |
| 113 | + "port": test_port, |
| 114 | + "url": f"http://localhost:{test_port}", |
| 115 | + "extension": extension, |
| 116 | + "listener": listener, |
| 117 | + } |
95 | 118 |
|
| 119 | + yield server_info |
96 | 120 |
|
97 | | -@pytest.fixture |
98 | | -def grpcbin_secure_port(grpcbin_extension): |
99 | | - """Return the secure (HTTP/2 with TLS) port for grpcbin.""" |
100 | | - return GRPCBIN_SECURE_PORT |
| 121 | + # Cleanup |
| 122 | + reactor.callFromThread(reactor.stop) |
| 123 | + time.sleep(0.5) |
| 124 | + extension.on_platform_shutdown() |
| 125 | + |
| 126 | + |
| 127 | +@pytest.fixture(scope="session") |
| 128 | +def grpcbin_extension(grpcbin_extension_server): |
| 129 | + """Return the extension instance from the server fixture.""" |
| 130 | + return grpcbin_extension_server["extension"] |
101 | 131 |
|
102 | 132 |
|
103 | 133 | def parse_server_frames(data: bytes) -> list: |
|
0 commit comments