diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 0dadd1855..c787190af 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -202,6 +202,15 @@
- [ ] update_vpc_link
+## apigatewaymanagementapi
+
+100% implemented
+
+- [X] delete_connection
+- [X] get_connection
+- [X] post_to_connection
+
+
## apigatewayv2
75% implemented
@@ -7246,7 +7255,6 @@
- amplify
- amplifybackend
- amplifyuibuilder
-- apigatewaymanagementapi
- appconfigdata
- appfabric
- appflow
diff --git a/docs/docs/services/apigatewaymanagementapi.rst b/docs/docs/services/apigatewaymanagementapi.rst
new file mode 100644
index 000000000..505438b48
--- /dev/null
+++ b/docs/docs/services/apigatewaymanagementapi.rst
@@ -0,0 +1,33 @@
+.. _implementedservice_apigatewaymanagementapi:
+
+.. |start-h3| raw:: html
+
+
+
+.. |end-h3| raw:: html
+
+
+
+=======================
+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
+
diff --git a/moto/__init__.py b/moto/__init__.py
index a16d598d3..18e69c159 100644
--- a/moto/__init__.py
+++ b/moto/__init__.py
@@ -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")
diff --git a/moto/apigatewaymanagementapi/__init__.py b/moto/apigatewaymanagementapi/__init__.py
new file mode 100644
index 000000000..0c986e2bd
--- /dev/null
+++ b/moto/apigatewaymanagementapi/__init__.py
@@ -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)
diff --git a/moto/apigatewaymanagementapi/models.py b/moto/apigatewaymanagementapi/models.py
new file mode 100644
index 000000000..9ea9b408b
--- /dev/null
+++ b/moto/apigatewaymanagementapi/models.py
@@ -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"
+)
diff --git a/moto/apigatewaymanagementapi/responses.py b/moto/apigatewaymanagementapi/responses.py
new file mode 100644
index 000000000..1accf478f
--- /dev/null
+++ b/moto/apigatewaymanagementapi/responses.py
@@ -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 "{}"
diff --git a/moto/apigatewaymanagementapi/urls.py b/moto/apigatewaymanagementapi/urls.py
new file mode 100644
index 000000000..f4a611047
--- /dev/null
+++ b/moto/apigatewaymanagementapi/urls.py
@@ -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[^/]+)$": response.dispatch,
+}
diff --git a/moto/backend_index.py b/moto/backend_index.py
index 477175fb2..5f57542fb 100644
--- a/moto/backend_index.py
+++ b/moto/backend_index.py
@@ -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")),
diff --git a/moto/moto_server/werkzeug_app.py b/moto/moto_server/werkzeug_app.py
index 1d196656a..5dea96f0c 100644
--- a/moto/moto_server/werkzeug_app.py
+++ b/moto/moto_server/werkzeug_app.py
@@ -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
diff --git a/tests/test_apigatewaymanagementapi/__init__.py b/tests/test_apigatewaymanagementapi/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_apigatewaymanagementapi/test_apigatewaymanagementapi.py b/tests/test_apigatewaymanagementapi/test_apigatewaymanagementapi.py
new file mode 100644
index 000000000..19f4c0839
--- /dev/null
+++ b/tests/test_apigatewaymanagementapi/test_apigatewaymanagementapi.py
@@ -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"