Feature: APIGateway Management API (#6588)
This commit is contained in:
parent
14f9c3d38a
commit
deb914fc54
@ -202,6 +202,15 @@
|
|||||||
- [ ] update_vpc_link
|
- [ ] update_vpc_link
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
## apigatewaymanagementapi
|
||||||
|
<details>
|
||||||
|
<summary>100% implemented</summary>
|
||||||
|
|
||||||
|
- [X] delete_connection
|
||||||
|
- [X] get_connection
|
||||||
|
- [X] post_to_connection
|
||||||
|
</details>
|
||||||
|
|
||||||
## apigatewayv2
|
## apigatewayv2
|
||||||
<details>
|
<details>
|
||||||
<summary>75% implemented</summary>
|
<summary>75% implemented</summary>
|
||||||
@ -7246,7 +7255,6 @@
|
|||||||
- amplify
|
- amplify
|
||||||
- amplifybackend
|
- amplifybackend
|
||||||
- amplifyuibuilder
|
- amplifyuibuilder
|
||||||
- apigatewaymanagementapi
|
|
||||||
- appconfigdata
|
- appconfigdata
|
||||||
- appfabric
|
- appfabric
|
||||||
- appflow
|
- appflow
|
||||||
|
33
docs/docs/services/apigatewaymanagementapi.rst
Normal file
33
docs/docs/services/apigatewaymanagementapi.rst
Normal 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
|
||||||
|
|
@ -29,6 +29,9 @@ mock_acm = lazy_load(".acm", "mock_acm")
|
|||||||
mock_acmpca = lazy_load(".acmpca", "mock_acmpca", boto3_name="acm-pca")
|
mock_acmpca = lazy_load(".acmpca", "mock_acmpca", boto3_name="acm-pca")
|
||||||
mock_amp = lazy_load(".amp", "mock_amp")
|
mock_amp = lazy_load(".amp", "mock_amp")
|
||||||
mock_apigateway = lazy_load(".apigateway", "mock_apigateway")
|
mock_apigateway = lazy_load(".apigateway", "mock_apigateway")
|
||||||
|
mock_apigatewaymanagementapi = lazy_load(
|
||||||
|
".apigatewaymanagementapi", "mock_apigatewaymanagementapi"
|
||||||
|
)
|
||||||
mock_apigatewayv2 = lazy_load(".apigatewayv2", "mock_apigatewayv2")
|
mock_apigatewayv2 = lazy_load(".apigatewayv2", "mock_apigatewayv2")
|
||||||
mock_appconfig = lazy_load(".appconfig", "mock_appconfig")
|
mock_appconfig = lazy_load(".appconfig", "mock_appconfig")
|
||||||
mock_appsync = lazy_load(".appsync", "mock_appsync")
|
mock_appsync = lazy_load(".appsync", "mock_appsync")
|
||||||
|
5
moto/apigatewaymanagementapi/__init__.py
Normal file
5
moto/apigatewaymanagementapi/__init__.py
Normal 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)
|
48
moto/apigatewaymanagementapi/models.py
Normal file
48
moto/apigatewaymanagementapi/models.py
Normal 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"
|
||||||
|
)
|
46
moto/apigatewaymanagementapi/responses.py
Normal file
46
moto/apigatewaymanagementapi/responses.py
Normal 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 "{}"
|
12
moto/apigatewaymanagementapi/urls.py
Normal file
12
moto/apigatewaymanagementapi/urls.py
Normal 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,
|
||||||
|
}
|
@ -6,6 +6,10 @@ backend_url_patterns = [
|
|||||||
("acm-pca", re.compile("https?://acm-pca\\.(.+)\\.amazonaws\\.com")),
|
("acm-pca", re.compile("https?://acm-pca\\.(.+)\\.amazonaws\\.com")),
|
||||||
("amp", re.compile("https?://aps\\.(.+)\\.amazonaws\\.com")),
|
("amp", re.compile("https?://aps\\.(.+)\\.amazonaws\\.com")),
|
||||||
("apigateway", re.compile("https?://apigateway\\.(.+)\\.amazonaws.com")),
|
("apigateway", re.compile("https?://apigateway\\.(.+)\\.amazonaws.com")),
|
||||||
|
(
|
||||||
|
"apigatewaymanagementapi",
|
||||||
|
re.compile("https?://execute-api\\.(.+)\\.amazonaws\\.com"),
|
||||||
|
),
|
||||||
("appconfig", re.compile("https?://appconfig\\.(.+)\\.amazonaws\\.com")),
|
("appconfig", re.compile("https?://appconfig\\.(.+)\\.amazonaws\\.com")),
|
||||||
(
|
(
|
||||||
"applicationautoscaling",
|
"applicationautoscaling",
|
||||||
@ -110,12 +114,9 @@ backend_url_patterns = [
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"meteringmarketplace",
|
"meteringmarketplace",
|
||||||
re.compile("https?://metering.marketplace\\.(.+)\\.amazonaws.com"),
|
re.compile("https?://metering.marketplace.(.+).amazonaws.com"),
|
||||||
),
|
|
||||||
(
|
|
||||||
"meteringmarketplace",
|
|
||||||
re.compile("https?://aws-marketplace\\.(.+)\\.amazonaws.com"),
|
|
||||||
),
|
),
|
||||||
|
("meteringmarketplace", re.compile("https?://aws-marketplace.(.+).amazonaws.com")),
|
||||||
("mq", re.compile("https?://mq\\.(.+)\\.amazonaws\\.com")),
|
("mq", re.compile("https?://mq\\.(.+)\\.amazonaws\\.com")),
|
||||||
("opsworks", re.compile("https?://opsworks\\.us-east-1\\.amazonaws.com")),
|
("opsworks", re.compile("https?://opsworks\\.us-east-1\\.amazonaws.com")),
|
||||||
("organizations", re.compile("https?://organizations\\.(.+)\\.amazonaws\\.com")),
|
("organizations", re.compile("https?://organizations\\.(.+)\\.amazonaws\\.com")),
|
||||||
|
@ -100,7 +100,14 @@ class DomainDispatcherApplication:
|
|||||||
try:
|
try:
|
||||||
credential_scope = auth.split(",")[0].split()[1]
|
credential_scope = auth.split(",")[0].split()[1]
|
||||||
_, _, region, service, _ = credential_scope.split("/")
|
_, _, 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()
|
service = service.lower()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Signature format does not match, this is exceptional and we can't
|
# Signature format does not match, this is exceptional and we can't
|
||||||
|
0
tests/test_apigatewaymanagementapi/__init__.py
Normal file
0
tests/test_apigatewaymanagementapi/__init__.py
Normal 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"
|
Loading…
Reference in New Issue
Block a user