Feature: APIGateway Management API (#6588)

This commit is contained in:
Bert Blommers 2023-08-10 22:02:37 +00:00 committed by GitHub
parent 14f9c3d38a
commit deb914fc54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 226 additions and 7 deletions

View File

@ -202,6 +202,15 @@
- [ ] update_vpc_link
</details>
## apigatewaymanagementapi
<details>
<summary>100% implemented</summary>
- [X] delete_connection
- [X] get_connection
- [X] post_to_connection
</details>
## apigatewayv2
<details>
<summary>75% implemented</summary>
@ -7246,7 +7255,6 @@
- amplify
- amplifybackend
- amplifyuibuilder
- apigatewaymanagementapi
- appconfigdata
- appfabric
- appflow

View File

@ -0,0 +1,33 @@
.. _implementedservice_apigatewaymanagementapi:
.. |start-h3| raw:: html
<h3>
.. |end-h3| raw:: html
</h3>
=======================
apigatewaymanagementapi
=======================
.. autoclass:: moto.apigatewaymanagementapi.models.ApiGatewayManagementApiBackend
|start-h3| Example usage |end-h3|
.. sourcecode:: python
@mock_apigatewaymanagementapi
def test_apigatewaymanagementapi_behaviour:
boto3.client("apigatewaymanagementapi")
...
|start-h3| Implemented features for this service |end-h3|
- [X] delete_connection
- [X] get_connection
- [X] post_to_connection

View File

@ -29,6 +29,9 @@ mock_acm = lazy_load(".acm", "mock_acm")
mock_acmpca = lazy_load(".acmpca", "mock_acmpca", boto3_name="acm-pca")
mock_amp = lazy_load(".amp", "mock_amp")
mock_apigateway = lazy_load(".apigateway", "mock_apigateway")
mock_apigatewaymanagementapi = lazy_load(
".apigatewaymanagementapi", "mock_apigatewaymanagementapi"
)
mock_apigatewayv2 = lazy_load(".apigatewayv2", "mock_apigatewayv2")
mock_appconfig = lazy_load(".appconfig", "mock_appconfig")
mock_appsync = lazy_load(".appsync", "mock_appsync")

View File

@ -0,0 +1,5 @@
"""apigatewaymanagementapi module initialization; sets value for base decorator."""
from .models import apigatewaymanagementapi_backends
from ..core.models import base_decorator
mock_apigatewaymanagementapi = base_decorator(apigatewaymanagementapi_backends)

View File

@ -0,0 +1,48 @@
"""ApiGatewayManagementApiBackend class with methods for supported APIs."""
from collections import defaultdict
from typing import Any, Dict
from moto.core import BaseBackend, BackendDict
from moto.core.utils import unix_time
class Connection:
def __init__(self) -> None:
self.connected_at = unix_time()
self.source_ip = "192.168.0.1"
self.user_agent = "Moto Mocks"
self.data = b""
def to_dict(self) -> Dict[str, Any]:
return {
"connectedAt": self.connected_at,
"lastActiveAt": unix_time(),
"identity": {
"sourceIp": self.source_ip,
"userAgent": self.user_agent,
},
}
class ApiGatewayManagementApiBackend(BaseBackend):
"""
Connecting to this API in ServerMode/Docker requires Python >= 3.8 and an up-to-date `werkzeug` version (>=2.3.x)
"""
def __init__(self, region_name: str, account_id: str):
super().__init__(region_name, account_id)
self.connections: Dict[str, Connection] = defaultdict(Connection)
def delete_connection(self, connection_id: str) -> None:
self.connections.pop(connection_id, None)
def get_connection(self, connection_id: str) -> Connection:
return self.connections[connection_id]
def post_to_connection(self, data: bytes, connection_id: str) -> None:
cnctn = self.get_connection(connection_id)
cnctn.data += data
apigatewaymanagementapi_backends = BackendDict(
ApiGatewayManagementApiBackend, "apigateway"
)

View File

@ -0,0 +1,46 @@
"""Handles incoming apigatewaymanagementapi requests, invokes methods, returns responses."""
import json
from typing import Any
from moto.core.responses import BaseResponse
from .models import apigatewaymanagementapi_backends, ApiGatewayManagementApiBackend
class ApiGatewayManagementApiResponse(BaseResponse):
"""Handler for ApiGatewayManagementApi requests and responses."""
def __init__(self) -> None:
super().__init__(service_name="apigatewaymanagementapi")
def setup_class(
self, request: Any, full_url: str, headers: Any, use_raw_body: bool = False
) -> None:
super().setup_class(request, full_url, headers, use_raw_body=True)
@property
def apigatewaymanagementapi_backend(self) -> ApiGatewayManagementApiBackend:
"""Return backend instance specific for this region."""
return apigatewaymanagementapi_backends[self.current_account][self.region]
def delete_connection(self) -> str:
connection_id = self.path.split("/@connections/")[-1]
self.apigatewaymanagementapi_backend.delete_connection(
connection_id=connection_id
)
return "{}"
def get_connection(self) -> str:
connection_id = self.path.split("/@connections/")[-1]
connection = self.apigatewaymanagementapi_backend.get_connection(
connection_id=connection_id
)
return json.dumps(connection.to_dict())
def post_to_connection(self) -> str:
connection_id = self.path.split("/@connections/")[-1]
data = self.body
self.apigatewaymanagementapi_backend.post_to_connection(
data=data,
connection_id=connection_id,
)
return "{}"

View File

@ -0,0 +1,12 @@
"""apigatewaymanagementapi base URL and path."""
from .responses import ApiGatewayManagementApiResponse
url_bases = [r"https?://execute-api\.(.+)\.amazonaws\.com"]
response = ApiGatewayManagementApiResponse()
url_paths = {
"{0}/@connections/(?P<connection_id>[^/]+)$": response.dispatch,
}

View File

@ -6,6 +6,10 @@ backend_url_patterns = [
("acm-pca", re.compile("https?://acm-pca\\.(.+)\\.amazonaws\\.com")),
("amp", re.compile("https?://aps\\.(.+)\\.amazonaws\\.com")),
("apigateway", re.compile("https?://apigateway\\.(.+)\\.amazonaws.com")),
(
"apigatewaymanagementapi",
re.compile("https?://execute-api\\.(.+)\\.amazonaws\\.com"),
),
("appconfig", re.compile("https?://appconfig\\.(.+)\\.amazonaws\\.com")),
(
"applicationautoscaling",
@ -110,12 +114,9 @@ backend_url_patterns = [
),
(
"meteringmarketplace",
re.compile("https?://metering.marketplace\\.(.+)\\.amazonaws.com"),
),
(
"meteringmarketplace",
re.compile("https?://aws-marketplace\\.(.+)\\.amazonaws.com"),
re.compile("https?://metering.marketplace.(.+).amazonaws.com"),
),
("meteringmarketplace", re.compile("https?://aws-marketplace.(.+).amazonaws.com")),
("mq", re.compile("https?://mq\\.(.+)\\.amazonaws\\.com")),
("opsworks", re.compile("https?://opsworks\\.us-east-1\\.amazonaws.com")),
("organizations", re.compile("https?://organizations\\.(.+)\\.amazonaws\\.com")),

View File

@ -100,7 +100,14 @@ class DomainDispatcherApplication:
try:
credential_scope = auth.split(",")[0].split()[1]
_, _, region, service, _ = credential_scope.split("/")
service = SIGNING_ALIASES.get(service.lower(), service)
path = environ.get("PATH_INFO", "")
if service.lower() == "execute-api" and path.startswith(
"/@connections"
):
# APIGateway Management API
pass
else:
service = SIGNING_ALIASES.get(service.lower(), service)
service = service.lower()
except ValueError:
# Signature format does not match, this is exceptional and we can't

View File

@ -0,0 +1,56 @@
"""Unit tests for apigatewaymanagementapi-supported APIs."""
import boto3
from moto import mock_apigatewaymanagementapi, settings
from moto.core.versions import is_werkzeug_2_3_x
from moto.apigatewaymanagementapi.models import apigatewaymanagementapi_backends
from tests import DEFAULT_ACCOUNT_ID
from unittest import SkipTest
# See our Development Tips on writing tests for hints on how to write good tests:
# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
@mock_apigatewaymanagementapi
def test_delete_connection():
if settings.TEST_SERVER_MODE and not is_werkzeug_2_3_x():
# URL matching changed between 2.2.x and 2.3.x
# 2.3.x has no problem matching the root path '/@connections', but 2.2.x refuses
raise SkipTest("Can't test this in older werkzeug versions")
client = boto3.client("apigatewaymanagementapi", region_name="eu-west-1")
# NO-OP
client.delete_connection(ConnectionId="anything")
@mock_apigatewaymanagementapi
def test_get_connection():
if settings.TEST_SERVER_MODE and not is_werkzeug_2_3_x():
# URL matching changed between 2.2.x and 2.3.x
# 2.3.x has no problem matching the root path '/@connections', but 2.2.x refuses
raise SkipTest("Can't test this in older werkzeug versions")
client = boto3.client("apigatewaymanagementapi", region_name="us-east-2")
conn = client.get_connection(ConnectionId="anything")
assert "ConnectedAt" in conn
assert conn["Identity"] == {"SourceIp": "192.168.0.1", "UserAgent": "Moto Mocks"}
assert "LastActiveAt" in conn
@mock_apigatewaymanagementapi
def test_post_to_connection():
if settings.TEST_SERVER_MODE and not is_werkzeug_2_3_x():
# URL matching changed between 2.2.x and 2.3.x
# 2.3.x has no problem matching the root path '/@connections', but 2.2.x refuses
raise SkipTest("Can't test this in older werkzeug versions")
client = boto3.client("apigatewaymanagementapi", region_name="ap-southeast-1")
client.post_to_connection(ConnectionId="anything", Data=b"my first bytes")
if not settings.TEST_SERVER_MODE:
backend = apigatewaymanagementapi_backends[DEFAULT_ACCOUNT_ID]["ap-southeast-1"]
assert backend.connections["anything"].data == b"my first bytes"
client.post_to_connection(ConnectionId="anything", Data=b"more bytes")
if not settings.TEST_SERVER_MODE:
backend = apigatewaymanagementapi_backends[DEFAULT_ACCOUNT_ID]["ap-southeast-1"]
assert backend.connections["anything"].data == b"my first bytesmore bytes"