Skip to content

Commit 111281c

Browse files
whummerclaude
andcommitted
add proxy tests for the API Gateway v2 service
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0330959 commit 111281c

2 files changed

Lines changed: 295 additions & 0 deletions

File tree

aws-proxy/aws_proxy/client/auth_proxy.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ def proxy_request(self, request: Request, data: bytes) -> Response:
8686
if not parsed:
8787
return requests_response("", status_code=400)
8888
region_name, service_name = parsed
89+
90+
# Map service names based on request context
91+
service_name = self._get_service_name(service_name, request.path)
92+
8993
query_string = to_str(request.query_string or "")
9094

9195
LOG.debug(
@@ -308,6 +312,16 @@ def _extract_region_and_service(self, headers) -> Optional[Tuple[str, str]]:
308312
return
309313
return parts[2], parts[3]
310314

315+
def _get_service_name(self, service_name: str, path: str) -> str:
316+
"""Map AWS signing service names to boto3 client names based on request context."""
317+
# API Gateway v2 uses 'apigateway' as signing name but needs 'apigatewayv2' client
318+
if service_name == "apigateway" and path.startswith("/v2/"):
319+
return "apigatewayv2"
320+
# CloudWatch uses 'monitoring' as signing name
321+
if service_name == "monitoring":
322+
return "cloudwatch"
323+
return service_name
324+
311325
@cache
312326
def _query_account_id_from_aws(self) -> str:
313327
session = boto3.Session()
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
# Note/disclosure: This file has been (partially or fully) generated by an AI agent.
2+
3+
import logging
4+
5+
import boto3
6+
import pytest
7+
from botocore.exceptions import ClientError
8+
from localstack.aws.connect import connect_to
9+
from localstack.utils.strings import short_uid
10+
11+
from aws_proxy.shared.models import ProxyConfig
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
def test_apigatewayv2_http_api_requests(start_aws_proxy, cleanups):
17+
"""Test basic API Gateway v2 HTTP API operations with proxy."""
18+
api_name_aws = f"test-http-api-{short_uid()}"
19+
20+
# start proxy - forwarding all API Gateway v2 requests
21+
config = ProxyConfig(services={"apigatewayv2": {"resources": ".*"}})
22+
start_aws_proxy(config)
23+
24+
# create clients
25+
region_name = "us-east-1"
26+
apigwv2_client = connect_to(region_name=region_name).apigatewayv2
27+
apigwv2_client_aws = boto3.client("apigatewayv2", region_name=region_name)
28+
29+
# create HTTP API in AWS
30+
create_response_aws = apigwv2_client_aws.create_api(
31+
Name=api_name_aws, ProtocolType="HTTP", Description="Test HTTP API for proxy"
32+
)
33+
api_id_aws = create_response_aws["ApiId"]
34+
cleanups.append(lambda: apigwv2_client_aws.delete_api(ApiId=api_id_aws))
35+
36+
# assert that local call for this API is proxied
37+
get_api_local = apigwv2_client.get_api(ApiId=api_id_aws)
38+
get_api_aws = apigwv2_client_aws.get_api(ApiId=api_id_aws)
39+
assert get_api_local["Name"] == get_api_aws["Name"] == api_name_aws
40+
assert get_api_local["ApiId"] == get_api_aws["ApiId"] == api_id_aws
41+
42+
# negative test: verify that requesting a non-existent API fails
43+
with pytest.raises(ClientError) as ctx:
44+
apigwv2_client_aws.get_api(ApiId="nonexistent123")
45+
assert ctx.value.response["Error"]["Code"] == "NotFoundException"
46+
47+
# list APIs from AWS should include the created API
48+
apis_aws = apigwv2_client_aws.get_apis()
49+
aws_api_ids = [api["ApiId"] for api in apis_aws.get("Items", [])]
50+
assert api_id_aws in aws_api_ids
51+
52+
# update API description via LocalStack client (should proxy to AWS)
53+
updated_description = "Updated description via proxy"
54+
apigwv2_client.update_api(ApiId=api_id_aws, Description=updated_description)
55+
56+
# verify update is reflected in AWS
57+
get_api_aws = apigwv2_client_aws.get_api(ApiId=api_id_aws)
58+
assert get_api_aws["Description"] == updated_description
59+
60+
61+
def test_apigatewayv2_routes_and_integrations(start_aws_proxy, cleanups):
62+
"""Test API Gateway v2 routes and integrations with proxy."""
63+
api_name_aws = f"test-http-api-routes-{short_uid()}"
64+
65+
# start proxy - forwarding all API Gateway v2 requests
66+
config = ProxyConfig(services={"apigatewayv2": {"resources": ".*"}})
67+
start_aws_proxy(config)
68+
69+
# create clients
70+
region_name = "us-east-1"
71+
apigwv2_client = connect_to(region_name=region_name).apigatewayv2
72+
apigwv2_client_aws = boto3.client("apigatewayv2", region_name=region_name)
73+
74+
# create HTTP API in AWS
75+
create_response_aws = apigwv2_client_aws.create_api(
76+
Name=api_name_aws, ProtocolType="HTTP"
77+
)
78+
api_id_aws = create_response_aws["ApiId"]
79+
cleanups.append(lambda: apigwv2_client_aws.delete_api(ApiId=api_id_aws))
80+
81+
# create integration via AWS client
82+
integration_response = apigwv2_client_aws.create_integration(
83+
ApiId=api_id_aws,
84+
IntegrationType="HTTP_PROXY",
85+
IntegrationMethod="GET",
86+
IntegrationUri="https://httpbin.org/get",
87+
PayloadFormatVersion="1.0",
88+
)
89+
integration_id = integration_response["IntegrationId"]
90+
91+
# verify integration via LocalStack (proxied)
92+
integration_local = apigwv2_client.get_integration(
93+
ApiId=api_id_aws, IntegrationId=integration_id
94+
)
95+
assert integration_local["IntegrationType"] == "HTTP_PROXY"
96+
assert integration_local["IntegrationId"] == integration_id
97+
98+
# create route via AWS client
99+
route_response = apigwv2_client_aws.create_route(
100+
ApiId=api_id_aws,
101+
RouteKey="GET /users",
102+
Target=f"integrations/{integration_id}",
103+
)
104+
route_id = route_response["RouteId"]
105+
106+
# verify route via LocalStack (proxied)
107+
route_local = apigwv2_client.get_route(ApiId=api_id_aws, RouteId=route_id)
108+
assert route_local["RouteKey"] == "GET /users"
109+
assert route_local["RouteId"] == route_id
110+
111+
# list routes via LocalStack (should proxy to AWS)
112+
routes_local = apigwv2_client.get_routes(ApiId=api_id_aws)
113+
route_ids_local = [r["RouteId"] for r in routes_local.get("Items", [])]
114+
assert route_id in route_ids_local
115+
116+
# delete route via AWS client
117+
apigwv2_client_aws.delete_route(ApiId=api_id_aws, RouteId=route_id)
118+
119+
# verify route is deleted via LocalStack (proxied)
120+
with pytest.raises(ClientError) as ctx:
121+
apigwv2_client.get_route(ApiId=api_id_aws, RouteId=route_id)
122+
assert ctx.value.response["Error"]["Code"] == "NotFoundException"
123+
124+
125+
def test_apigatewayv2_stages_and_deployments(start_aws_proxy, cleanups):
126+
"""Test API Gateway v2 stages and deployments with proxy."""
127+
api_name_aws = f"test-http-api-deploy-{short_uid()}"
128+
129+
# start proxy - forwarding all API Gateway v2 requests
130+
config = ProxyConfig(services={"apigatewayv2": {"resources": ".*"}})
131+
start_aws_proxy(config)
132+
133+
# create clients
134+
region_name = "us-east-1"
135+
apigwv2_client = connect_to(region_name=region_name).apigatewayv2
136+
apigwv2_client_aws = boto3.client("apigatewayv2", region_name=region_name)
137+
138+
# create HTTP API in AWS
139+
create_response_aws = apigwv2_client_aws.create_api(
140+
Name=api_name_aws, ProtocolType="HTTP"
141+
)
142+
api_id_aws = create_response_aws["ApiId"]
143+
cleanups.append(lambda: apigwv2_client_aws.delete_api(ApiId=api_id_aws))
144+
145+
# create a simple integration and route (required for deployment)
146+
integration_response = apigwv2_client_aws.create_integration(
147+
ApiId=api_id_aws,
148+
IntegrationType="HTTP_PROXY",
149+
IntegrationMethod="GET",
150+
IntegrationUri="https://httpbin.org/get",
151+
PayloadFormatVersion="1.0",
152+
)
153+
integration_id = integration_response["IntegrationId"]
154+
155+
apigwv2_client_aws.create_route(
156+
ApiId=api_id_aws,
157+
RouteKey="GET /test",
158+
Target=f"integrations/{integration_id}",
159+
)
160+
161+
# create stage via LocalStack client (should proxy to AWS)
162+
stage_name = "test"
163+
apigwv2_client.create_stage(
164+
ApiId=api_id_aws, StageName=stage_name, Description="Test stage"
165+
)
166+
167+
# verify stage exists in AWS
168+
stage_aws = apigwv2_client_aws.get_stage(ApiId=api_id_aws, StageName=stage_name)
169+
assert stage_aws["StageName"] == stage_name
170+
assert stage_aws["Description"] == "Test stage"
171+
172+
# get stage via LocalStack client
173+
stage_local = apigwv2_client.get_stage(ApiId=api_id_aws, StageName=stage_name)
174+
assert stage_local["StageName"] == stage_aws["StageName"]
175+
176+
# create deployment via AWS client
177+
deployment_response = apigwv2_client_aws.create_deployment(
178+
ApiId=api_id_aws, StageName=stage_name, Description="Test deployment"
179+
)
180+
deployment_id = deployment_response["DeploymentId"]
181+
182+
# verify deployment via LocalStack (proxied)
183+
deployment_local = apigwv2_client.get_deployment(
184+
ApiId=api_id_aws, DeploymentId=deployment_id
185+
)
186+
assert deployment_local["DeploymentId"] == deployment_id
187+
188+
# list stages via AWS client
189+
stages_aws = apigwv2_client_aws.get_stages(ApiId=api_id_aws)
190+
stage_names_aws = [s["StageName"] for s in stages_aws.get("Items", [])]
191+
assert stage_name in stage_names_aws
192+
193+
194+
def test_apigatewayv2_read_only_mode(start_aws_proxy, cleanups):
195+
"""Test API Gateway v2 operations in read-only proxy mode."""
196+
api_name_aws = f"test-http-api-readonly-{short_uid()}"
197+
198+
# create HTTP API in AWS first (before starting proxy)
199+
region_name = "us-east-1"
200+
apigwv2_client_aws = boto3.client("apigatewayv2", region_name=region_name)
201+
create_response_aws = apigwv2_client_aws.create_api(
202+
Name=api_name_aws, ProtocolType="HTTP"
203+
)
204+
api_id_aws = create_response_aws["ApiId"]
205+
cleanups.append(lambda: apigwv2_client_aws.delete_api(ApiId=api_id_aws))
206+
207+
# start proxy in read-only mode
208+
config = ProxyConfig(
209+
services={"apigatewayv2": {"resources": ".*", "read_only": True}}
210+
)
211+
start_aws_proxy(config)
212+
213+
# create LocalStack client
214+
apigwv2_client = connect_to(region_name=region_name).apigatewayv2
215+
216+
# read operations should work (proxied to AWS)
217+
get_api_local = apigwv2_client.get_api(ApiId=api_id_aws)
218+
assert get_api_local["Name"] == api_name_aws
219+
assert get_api_local["ApiId"] == api_id_aws
220+
221+
# verify the API can also be read directly from AWS
222+
get_api_aws = apigwv2_client_aws.get_api(ApiId=api_id_aws)
223+
assert get_api_local["Name"] == get_api_aws["Name"]
224+
225+
# write operations should not be proxied in read-only mode
226+
original_description = get_api_aws.get("Description", "")
227+
228+
# Attempt write operation - should either be blocked or not proxied
229+
try:
230+
apigwv2_client.update_api(ApiId=api_id_aws, Description="Should not reach AWS")
231+
except Exception as e:
232+
logger.info(f"Read-only mode blocked write operation: {e}")
233+
234+
# Verify the API description was not changed in AWS
235+
get_api_aws_after = apigwv2_client_aws.get_api(ApiId=api_id_aws)
236+
assert get_api_aws_after.get("Description", "") == original_description
237+
238+
239+
def test_apigatewayv2_websocket_api(start_aws_proxy, cleanups):
240+
"""Test API Gateway v2 WebSocket API operations with proxy."""
241+
api_name_aws = f"test-websocket-api-{short_uid()}"
242+
243+
# start proxy - forwarding all API Gateway v2 requests
244+
config = ProxyConfig(services={"apigatewayv2": {"resources": ".*"}})
245+
start_aws_proxy(config)
246+
247+
# create clients
248+
region_name = "us-east-1"
249+
apigwv2_client = connect_to(region_name=region_name).apigatewayv2
250+
apigwv2_client_aws = boto3.client("apigatewayv2", region_name=region_name)
251+
252+
# create WebSocket API in AWS
253+
create_response_aws = apigwv2_client_aws.create_api(
254+
Name=api_name_aws,
255+
ProtocolType="WEBSOCKET",
256+
RouteSelectionExpression="$request.body.action",
257+
)
258+
api_id_aws = create_response_aws["ApiId"]
259+
cleanups.append(lambda: apigwv2_client_aws.delete_api(ApiId=api_id_aws))
260+
261+
# assert that local call for this API is proxied
262+
get_api_local = apigwv2_client.get_api(ApiId=api_id_aws)
263+
get_api_aws = apigwv2_client_aws.get_api(ApiId=api_id_aws)
264+
assert get_api_local["Name"] == get_api_aws["Name"] == api_name_aws
265+
assert get_api_local["ProtocolType"] == "WEBSOCKET"
266+
267+
# create a route for WebSocket API via AWS client
268+
route_response = apigwv2_client_aws.create_route(
269+
ApiId=api_id_aws, RouteKey="$connect"
270+
)
271+
route_id = route_response["RouteId"]
272+
273+
# verify route via LocalStack (proxied)
274+
route_local = apigwv2_client.get_route(ApiId=api_id_aws, RouteId=route_id)
275+
assert route_local["RouteKey"] == "$connect"
276+
assert route_local["RouteId"] == route_id
277+
278+
# list routes should include the $connect route
279+
routes_local = apigwv2_client.get_routes(ApiId=api_id_aws)
280+
route_keys = [r["RouteKey"] for r in routes_local.get("Items", [])]
281+
assert "$connect" in route_keys

0 commit comments

Comments
 (0)