Skip to content

Commit f2d65e3

Browse files
whummerclaude
andcommitted
add tests for utils package using grpcbin
- Add unit tests for HTTP/2 frame parsing and TcpForwarder (33 tests) - Add integration tests using grpcbin Docker container (19 tests) - Move test_get_frames_from_http2_stream from typedb to utils - Add test dependencies and pytest markers to pyproject.toml - Add test targets to Makefile (test, test-unit, test-integration) - Add proto files for grpcbin service definitions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0e5c01f commit f2d65e3

File tree

15 files changed

+1193
-20
lines changed

15 files changed

+1193
-20
lines changed

typedb/tests/test_extension.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import requests
22
import httpx
33
from localstack.utils.strings import short_uid
4-
from localstack_extensions.utils import (
5-
get_frames_from_http2_stream,
6-
get_headers_from_frames,
7-
)
84
from typedb.driver import TypeDB, Credentials, DriverOptions, TransactionType
95

106

@@ -98,17 +94,3 @@ def test_connect_to_h2_endpoint_non_typedb():
9894
assert response.status_code == 200
9995
assert response.http_version == "HTTP/2"
10096
assert "<ListAllMyBucketsResult" in response.text
101-
102-
103-
def test_get_frames_from_http2_stream():
104-
# note: the data below is a dump taken from a browser request made against the emulator
105-
data = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n\x00\x00\x18\x04\x00\x00\x00\x00\x00\x00\x01\x00\x01\x00\x00\x00\x02\x00\x00\x00\x00\x00\x04\x00\x02\x00\x00\x00\x05\x00\x00@\x00\x00\x00\x04\x08\x00\x00\x00\x00\x00\x00\xbf\x00\x01"
106-
data += b"\x00\x01V\x01%\x00\x00\x00\x03\x00\x00\x00\x00\x15C\x87\xd5\xaf~MZw\x7f\x05\x8eb*\x0eA\xd0\x84\x8c\x9dX\x9c\xa3\xa13\xffA\x96\xa0\xe4\x1d\x13\x9d\t^\x83\x90t!#'U\xc9A\xed\x92\xe3M\xb8\xe7\x87z\xbe\xd0\x7ff\xa2\x81\xb0\xda\xe0S\xfa\xd02\x1a\xa4\x9d\x13\xfd\xa9\x92\xa4\x96\x854\x0c\x8aj\xdc\xa7\xe2\x81\x02\xe1o\xedK;\xdc\x0bM.\x0f\xedLE'S\xb0 \x04\x00\x08\x02\xa6\x13XYO\xe5\x80\xb4\xd2\xe0S\x83\xf9c\xe7Q\x8b-Kp\xdd\xf4Z\xbe\xfb@\x05\xdbP\x92\x9b\xd9\xab\xfaRB\xcb@\xd2_\xa5#\xb3\xe9OhL\x9f@\x94\x19\x08T!b\x1e\xa4\xd8z\x16\xb0\xbd\xad*\x12\xb5%L\xe7\x93\x83\xc5\x83\x7f@\x95\x19\x08T!b\x1e\xa4\xd8z\x16\xb0\xbd\xad*\x12\xb4\xe5\x1c\x85\xb1\x1f\x89\x1d\xa9\x9c\xf6\x1b\xd8\xd2c\xd5s\x95\x9d)\xad\x17\x18`u\xd6\xbd\x07 \xe8BFN\xab\x92\x83\xdb#\x1f@\x85=\x86\x98\xd5\x7f\x94\x9d)\xad\x17\x18`u\xd6\xbd\x07 \xe8BFN\xab\x92\x83\xdb'@\x8aAH\xb4\xa5I'ZB\xa1?\x84-5\xa7\xd7@\x8aAH\xb4\xa5I'Z\x93\xc8_\x83!\xecG@\x8aAH\xb4\xa5I'Y\x06I\x7f\x86@\xe9*\xc82K@\x86\xae\xc3\x1e\xc3'\xd7\x83\xb6\x06\xbf@\x82I\x7f\x86M\x835\x05\xb1\x1f\x00\x00\x04\x08\x00\x00\x00\x00\x03\x00\xbe\x00\x00"
107-
108-
frames = get_frames_from_http2_stream(data)
109-
assert frames
110-
headers = get_headers_from_frames(frames)
111-
assert headers
112-
assert headers[":scheme"] == "https"
113-
assert headers[":method"] == "OPTIONS"
114-
assert headers[":path"] == "/_localstack/health"

utils/Makefile

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ venv: $(VENV_ACTIVATE)
1111
$(VENV_ACTIVATE): pyproject.toml
1212
test -d .venv || $(VENV_BIN) .venv
1313
$(VENV_RUN); pip install --upgrade pip setuptools wheel build
14-
$(VENV_RUN); pip install -e .[dev]
14+
$(VENV_RUN); pip install -e .[dev,test]
1515
touch $(VENV_DIR)/bin/activate
1616

1717
clean: ## Clean up build artifacts and virtual environment
@@ -34,7 +34,23 @@ format: venv ## Run ruff to format and fix the code
3434
lint: venv ## Run ruff to lint the code
3535
$(VENV_RUN); python -m ruff check --output-format=full .
3636

37+
test: venv ## Run all tests
38+
$(VENV_RUN); python -m pytest tests/ -v
39+
40+
test-unit: venv ## Run unit tests only (no Docker required)
41+
$(VENV_RUN); python -m pytest tests/unit/ -v -m unit
42+
43+
test-integration: venv ## Run integration tests (Docker required)
44+
$(VENV_RUN); python -m pytest tests/integration/ -v -m integration
45+
46+
proto: venv ## Generate Python stubs from .proto files
47+
$(VENV_RUN); python -m grpc_tools.protoc \
48+
-I./proto \
49+
--python_out=./tests/proto \
50+
--grpc_python_out=./tests/proto \
51+
./proto/*.proto
52+
3753
clean-dist: clean
3854
rm -rf dist/
3955

40-
.PHONY: clean clean-dist dist install publish usage venv format lint
56+
.PHONY: clean clean-dist dist install publish usage venv format lint test test-unit test-integration proto

utils/proto/grpcbin.proto

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// grpcbin.proto - Service definitions for moul/grpcbin
2+
// These are the main services exposed by the grpcbin test server
3+
// Source: https://github.com/moul/grpcbin
4+
5+
syntax = "proto3";
6+
7+
package grpcbin;
8+
9+
option go_package = "grpcbin";
10+
11+
// Empty message for simple requests
12+
message EmptyMessage {}
13+
14+
// DummyMessage for testing
15+
message DummyMessage {
16+
string f_string = 1;
17+
repeated string f_strings = 2;
18+
int32 f_int32 = 3;
19+
repeated int32 f_int32s = 4;
20+
int32 f_enum = 5;
21+
DummyMessage f_sub = 6;
22+
bool f_bool = 7;
23+
int64 f_int64 = 8;
24+
repeated int64 f_int64s = 9;
25+
double f_double = 10;
26+
float f_float = 11;
27+
}
28+
29+
// GRPCBin service provides various RPC methods for testing
30+
service GRPCBin {
31+
// Index returns an empty response
32+
rpc Index (EmptyMessage) returns (EmptyMessage) {}
33+
34+
// DummyUnary echoes back the request
35+
rpc DummyUnary (DummyMessage) returns (DummyMessage) {}
36+
37+
// DummyServerStream streams back the request multiple times
38+
rpc DummyServerStream (DummyMessage) returns (stream DummyMessage) {}
39+
40+
// DummyClientStream receives multiple messages and returns a summary
41+
rpc DummyClientStream (stream DummyMessage) returns (DummyMessage) {}
42+
43+
// DummyBidirectionalStreamPing echoes back each message
44+
rpc DummyBidirectionalStreamPing (stream DummyMessage) returns (stream DummyMessage) {}
45+
}

utils/proto/hello.proto

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// hello.proto - Simple HelloService for grpcbin
2+
// Source: https://github.com/moul/grpcbin
3+
4+
syntax = "proto3";
5+
6+
package hello;
7+
8+
option go_package = "hello";
9+
10+
// HelloRequest contains the name to greet
11+
message HelloRequest {
12+
string greeting = 1;
13+
}
14+
15+
// HelloResponse contains the greeting response
16+
message HelloResponse {
17+
string reply = 1;
18+
}
19+
20+
// HelloService provides a simple greeting RPC
21+
service HelloService {
22+
// SayHello returns a greeting
23+
rpc SayHello (HelloRequest) returns (HelloResponse) {}
24+
}

utils/pyproject.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,22 @@ dev = [
3535
"pytest",
3636
"ruff",
3737
]
38+
test = [
39+
"pytest>=7.0",
40+
"pytest-timeout>=2.0",
41+
"grpcio>=1.50.0",
42+
"grpcio-tools>=1.50.0",
43+
]
3844

3945
[tool.setuptools.packages.find]
4046
include = ["localstack_extensions*"]
47+
48+
[tool.pytest.ini_options]
49+
testpaths = ["tests"]
50+
markers = [
51+
"unit: Unit tests (no Docker/LocalStack required)",
52+
"integration: Integration tests (Docker required, no LocalStack)",
53+
]
54+
filterwarnings = [
55+
"ignore::DeprecationWarning",
56+
]

utils/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Utils package tests

utils/tests/conftest.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""
2+
Shared pytest configuration for the utils package tests.
3+
4+
Test categories:
5+
- unit: No Docker or LocalStack required (pure functions with mocks)
6+
- integration: Docker required (uses grpcbin), no LocalStack
7+
"""
8+
9+
import pytest
10+
11+
12+
def pytest_configure(config):
13+
"""Register custom markers."""
14+
config.addinivalue_line("markers", "unit: Unit tests (no Docker/LocalStack required)")
15+
config.addinivalue_line(
16+
"markers", "integration: Integration tests (Docker required, no LocalStack)"
17+
)
18+
19+
20+
def pytest_collection_modifyitems(config, items):
21+
"""
22+
Automatically mark tests based on their location in the test directory.
23+
Tests in tests/unit/ are marked as 'unit'.
24+
Tests in tests/integration/ are marked as 'integration'.
25+
"""
26+
for item in items:
27+
# Get the path relative to the tests directory
28+
test_path = str(item.fspath)
29+
30+
if "/tests/unit/" in test_path:
31+
item.add_marker(pytest.mark.unit)
32+
elif "/tests/integration/" in test_path:
33+
item.add_marker(pytest.mark.integration)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Integration tests - Docker required, no LocalStack
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"""
2+
Integration test fixtures for utils package.
3+
4+
Provides fixtures for running tests against the grpcbin Docker container.
5+
grpcbin is a neutral gRPC test service that supports various RPC types.
6+
"""
7+
8+
import subprocess
9+
import time
10+
import socket
11+
import pytest
12+
13+
14+
GRPCBIN_IMAGE = "moul/grpcbin"
15+
GRPCBIN_INSECURE_PORT = 9000 # HTTP/2 without TLS
16+
GRPCBIN_SECURE_PORT = 9001 # HTTP/2 with TLS
17+
18+
19+
def is_port_open(host: str, port: int, timeout: float = 1.0) -> bool:
20+
"""Check if a port is open and accepting connections."""
21+
try:
22+
with socket.create_connection((host, port), timeout=timeout):
23+
return True
24+
except (socket.timeout, socket.error, ConnectionRefusedError, OSError):
25+
return False
26+
27+
28+
def wait_for_port(host: str, port: int, timeout: float = 30.0) -> bool:
29+
"""Wait for a port to become available."""
30+
start_time = time.time()
31+
while time.time() - start_time < timeout:
32+
if is_port_open(host, port):
33+
return True
34+
time.sleep(0.5)
35+
return False
36+
37+
38+
@pytest.fixture(scope="session")
39+
def grpcbin_container():
40+
"""
41+
Start a grpcbin Docker container for testing.
42+
43+
The container exposes:
44+
- Port 9000: Insecure gRPC (HTTP/2 without TLS)
45+
- Port 9001: Secure gRPC (HTTP/2 with TLS)
46+
47+
The container is automatically removed after tests complete.
48+
"""
49+
container_name = "pytest-grpcbin"
50+
51+
# Check if Docker is available
52+
try:
53+
subprocess.run(
54+
["docker", "info"],
55+
capture_output=True,
56+
check=True,
57+
timeout=10,
58+
)
59+
except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
60+
pytest.skip("Docker is not available")
61+
62+
# Remove any existing container with the same name
63+
subprocess.run(
64+
["docker", "rm", "-f", container_name],
65+
capture_output=True,
66+
timeout=30,
67+
)
68+
69+
# Start the container
70+
result = subprocess.run(
71+
[
72+
"docker",
73+
"run",
74+
"-d",
75+
"--rm",
76+
"--name",
77+
container_name,
78+
"-p",
79+
f"{GRPCBIN_INSECURE_PORT}:{GRPCBIN_INSECURE_PORT}",
80+
"-p",
81+
f"{GRPCBIN_SECURE_PORT}:{GRPCBIN_SECURE_PORT}",
82+
GRPCBIN_IMAGE,
83+
],
84+
capture_output=True,
85+
text=True,
86+
timeout=60,
87+
)
88+
89+
if result.returncode != 0:
90+
pytest.fail(f"Failed to start grpcbin container: {result.stderr}")
91+
92+
container_id = result.stdout.strip()
93+
94+
# Wait for the insecure port to be ready
95+
if not wait_for_port("localhost", GRPCBIN_INSECURE_PORT, timeout=30):
96+
# Clean up and fail
97+
subprocess.run(["docker", "rm", "-f", container_name], capture_output=True)
98+
pytest.fail(f"grpcbin port {GRPCBIN_INSECURE_PORT} did not become available")
99+
100+
# Give the gRPC server inside the container a moment to fully initialize
101+
# The port may be open before the HTTP/2 server is ready to process requests
102+
time.sleep(1.0)
103+
104+
# Provide connection info to tests
105+
yield {
106+
"container_id": container_id,
107+
"container_name": container_name,
108+
"host": "localhost",
109+
"insecure_port": GRPCBIN_INSECURE_PORT,
110+
"secure_port": GRPCBIN_SECURE_PORT,
111+
}
112+
113+
# Cleanup: stop and remove the container
114+
subprocess.run(
115+
["docker", "rm", "-f", container_name],
116+
capture_output=True,
117+
timeout=30,
118+
)
119+
120+
121+
@pytest.fixture
122+
def grpcbin_host(grpcbin_container):
123+
"""Return the host address for the grpcbin container."""
124+
return grpcbin_container["host"]
125+
126+
127+
@pytest.fixture
128+
def grpcbin_insecure_port(grpcbin_container):
129+
"""Return the insecure (HTTP/2 without TLS) port for grpcbin."""
130+
return grpcbin_container["insecure_port"]
131+
132+
133+
@pytest.fixture
134+
def grpcbin_secure_port(grpcbin_container):
135+
"""Return the secure (HTTP/2 with TLS) port for grpcbin."""
136+
return grpcbin_container["secure_port"]

0 commit comments

Comments
 (0)