Repository: getmoto/moto
Pull request #8898 (2025-05-14 20:39:54)

Adds AppSync Events API support
# AppSync Events API Methods Implementation

This PR covers some of the methods for the AppSync recently launched Events API -> [https://docs.aws.amazon.com/appsync/latest/eventapi/create-event-api-tutorial.html](https://docs.aws.amazon.com/appsync/latest/eventapi/create-event-api-tutorial.html)

The methods covered are:
- [x] `create_api`
- [x] `list_apis`
- [x] `delete_api`
- [x] `create_channel_namespace`
- [x] `list_channel_namespaces`
- [x] `delete_channel_namespace`
- [x] `get_api`



ThiagoDiasV: One question that I have, when we create a new AppSync Events API, when we send a HTTP POST to the API with some event to be published to an specific channel, a common response when the request was successful is like 

```
{"failed": [], "successful": [{"identifier": str(uuid.uuid4()), "index": 0}]}
```

But I'm not sure if we can make `moto` able to handle this kind of behavior under the hood without `responses_mock` in the test itself. I created a test to show the entire flow, which is:

```
@mock_aws
def test_events_api_direct_http_request_entire_flow():
    client = boto3.client("appsync", region_name="us-east-1")
    api_resp = client.create_api(
        name="events-api",
        eventConfig={
            "authProviders": [
                {"authType": "API_KEY"},
            ],
            "connectionAuthModes": [
                {"authType": "API_KEY"},
            ],
            "defaultPublishAuthModes": [
                {"authType": "API_KEY"},
            ],
            "defaultSubscribeAuthModes": [
                {"authType": "API_KEY"},
            ],
        },
    )
    api_id = api_resp["api"]["apiId"]
    api_http_url = api_resp["api"]["dns"]["HTTP"]

    channel_name = "test-channel/"
    client.create_channel_namespace(
        apiId=api_id,
        name=channel_name,
        subscribeAuthModes=[{"authType": "API_KEY"}],
        publishAuthModes=[{"authType": "API_KEY"}],
    )

    tomorrow = datetime.now() + timedelta(days=1)
    tomorrow_in_secs = int(unix_time(tomorrow))
    key_resp = client.create_api_key(
        apiId=api_id, description="test api key", expires=tomorrow_in_secs
    )
    api_key = key_resp["apiKey"]["id"]

    foo_content = "test-foo-123"
    bar_content = "test-bar-456"
    event_payload = {
        "channel": channel_name,
        "events": [json.dumps({"foo": foo_content, "bar": bar_content}, default=str)],
    }

    endpoint_url = f"https://{api_http_url}/event"
    response_content = json.dumps(
        {"failed": [], "successful": [{"identifier": str(uuid.uuid4()), "index": 0}]}
    )
    responses_mock.add(responses_mock.POST, endpoint_url, body=response_content)
    registered = responses_mock.registered()
    assert isinstance(registered, list) and len(registered) > 1

    response = requests.post(
        endpoint_url,
        headers={"Content-Type": "application/json", "x-api-key": api_key},
        json=event_payload,
        timeout=10,
    )
    assert response.status_code == 200
    assert response.json() == json.loads(response_content)

    get_api_resp = client.get_api(apiId=api_id)
    assert get_api_resp["api"]["apiId"] == api_id
    assert get_api_resp["api"]["dns"]["HTTP"] == api_http_url

    list_channels_resp = client.list_channel_namespaces(apiId=api_id)
    assert len(list_channels_resp["channelNamespaces"]) == 1
    assert list_channels_resp["channelNamespaces"][0]["name"] == channel_name

    list_keys_resp = client.list_api_keys(apiId=api_id)
    assert len(list_keys_resp["apiKeys"]) == 1
    assert list_keys_resp["apiKeys"][0]["id"] == api_key

```

Not sure if we can add a new URL pattern in `urls.py` like this 

```
url_bases = [
    r"https?://appsync\.(.+)\.amazonaws\.com",
    r"https?://([a-zA-Z0-9\-_]+)\.appsync-api\.(.+)\.amazonaws\.com",  # Add this line to match the created events API DNS
]

...

url_paths = {
...
"{0}/event$": AppSyncResponse.method_dispatch(AppSyncResponse.dns_event_response)  # Add this line
}
```

With a `def event` in `responses.py` to handle mocked requests to the new events API, which would be the ideal scenario so the test could be written directly like this, without `responses_mock`

```

@mock_aws
def test_events_api_direct_http_request_entire_flow():
    client = boto3.client("appsync", region_name="us-east-1")
    api_resp = client.create_api(
        name="events-api",
        eventConfig={
            "authProviders": [
                {"authType": "API_KEY"},
            ],
            "connectionAuthModes": [
                {"authType": "API_KEY"},
            ],
            "defaultPublishAuthModes": [
                {"authType": "API_KEY"},
            ],
            "defaultSubscribeAuthModes": [
                {"authType": "API_KEY"},
            ],
        },
    )
    api_id = api_resp["api"]["apiId"]
    api_http_url = api_resp["api"]["dns"]["HTTP"]

    channel_name = "test-channel/"
    client.create_channel_namespace(
        apiId=api_id,
        name=channel_name,
        subscribeAuthModes=[{"authType": "API_KEY"}],
        publishAuthModes=[{"authType": "API_KEY"}],
    )

    tomorrow = datetime.now() + timedelta(days=1)
    tomorrow_in_secs = int(unix_time(tomorrow))
    key_resp = client.create_api_key(
        apiId=api_id, description="test api key", expires=tomorrow_in_secs
    )
    api_key = key_resp["apiKey"]["id"]

    foo_content = "test-foo-123"
    bar_content = "test-bar-456"
    event_payload = {
        "channel": channel_name,
        "events": [json.dumps({"foo": foo_content, "bar": bar_content}, default=str)],
    }

    endpoint_url = f"https://{api_http_url}/event"
    response_content = json.dumps(
        {"failed": [], "successful": [{"identifier": str(uuid.uuid4()), "index": 0}]}
    )
    # REMOVE THESE LINES, THE HTTP RESPONSE WOULD WORK UNDER THE HOOD
    # responses_mock.add(responses_mock.POST, endpoint_url, body=response_content)
    # registered = responses_mock.registered()
    # assert isinstance(registered, list) and len(registered) > 1

    response = requests.post(
        endpoint_url,
        headers={"Content-Type": "application/json", "x-api-key": api_key},
        json=event_payload,
        timeout=10,
    )
    assert response.status_code == 200
    assert response.json()["failed"] == []
    assert response.json()["successful"]

    get_api_resp = client.get_api(apiId=api_id)
    assert get_api_resp["api"]["apiId"] == api_id
    assert get_api_resp["api"]["dns"]["HTTP"] == api_http_url

    list_channels_resp = client.list_channel_namespaces(apiId=api_id)
    assert len(list_channels_resp["channelNamespaces"]) == 1
    assert list_channels_resp["channelNamespaces"][0]["name"] == channel_name

    list_keys_resp = client.list_api_keys(apiId=api_id)
    assert len(list_keys_resp["apiKeys"]) == 1
    assert list_keys_resp["apiKeys"][0]["id"] == api_key

```

I tried something like that but that wasn't working, the request was returning a `400` status code with content equals to `The method is not implemented`. 
ThiagoDiasV: > Hi @ThiagoDiasV, thanks for the PR! This mostly looks good - I've suggested a change here to get around the part where you have to manually mock the Events-url:
> 
> [ThiagoDiasV#1](https://github.com/ThiagoDiasV/moto/pull/1)

Thanks @bblommers for your PR and for your explanations. Now it's more clear how I would need to handle that scenario with the events url. I already merged your PR and added a couple of validations for `create_channel_namespace` and `create_api` calls. 


Patch:
diff --git a/moto/appsync/exceptions.py b/moto/appsync/exceptions.py
index d3ffd2a97..0b8f34f12 100644
--- a/moto/appsync/exceptions.py
+++ b/moto/appsync/exceptions.py
@@ -7,6 +7,22 @@ class AppSyncExceptions(JsonRESTError):
     pass
 
 
+class AWSValidationException(AppSyncExceptions):
+    code = 400
+
+    def __init__(self, message: str):
+        super().__init__("ValidationException", message)
+        self.description = json.dumps({"message": self.message})
+
+
+class ApiKeyValidityOutOfBoundsException(AppSyncExceptions):
+    code = 400
+
+    def __init__(self, message: str):
+        super().__init__("ApiKeyValidityOutOfBoundsException", message)
+        self.description = json.dumps({"message": self.message})
+
+
 class GraphqlAPINotFound(AppSyncExceptions):
     code = 404
 
@@ -34,6 +50,14 @@ class GraphqlAPICacheNotFound(AppSyncExceptions):
         self.description = json.dumps({"message": self.message})
 
 
+class EventsAPINotFound(AppSyncExceptions):
+    code = 404
+
+    def __init__(self, api_id: str):
+        super().__init__("NotFoundException", f"Events API {api_id} not found.")
+        self.description = json.dumps({"message": self.message})
+
+
 class BadRequestException(AppSyncExceptions):
     def __init__(self, message: str):
         super().__init__("BadRequestException", message)
diff --git a/moto/appsync/models.py b/moto/appsync/models.py
index 1e6d56c66..8d715920c 100644
--- a/moto/appsync/models.py
+++ b/moto/appsync/models.py
@@ -1,7 +1,7 @@
 import base64
 import json
 from datetime import datetime, timedelta, timezone
-from typing import Any, Dict, Iterable, List, Optional, Tuple
+from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
 
 from moto.core.base_backend import BackendDict, BaseBackend
 from moto.core.common_models import BaseModel
@@ -12,6 +12,7 @@ from moto.utilities.utils import get_partition
 
 from .exceptions import (
     BadRequestException,
+    EventsAPINotFound,
     GraphqlAPICacheNotFound,
     GraphqlAPINotFound,
     GraphQLSchemaException,
@@ -45,6 +46,54 @@ directive @aws_cognito_user_pools(
 """
 
 
+# region: APICache
+class APICache(BaseModel):
+    def __init__(
+        self,
+        ttl: int,
+        api_caching_behavior: str,
+        type_: str,
+        transit_encryption_enabled: Optional[bool] = None,
+        at_rest_encryption_enabled: Optional[bool] = None,
+        health_metrics_config: Optional[str] = None,
+    ):
+        self.ttl = ttl
+        self.api_caching_behavior = api_caching_behavior
+        self.type = type_
+        self.transit_encryption_enabled = transit_encryption_enabled or False
+        self.at_rest_encryption_enabled = at_rest_encryption_enabled or False
+        self.health_metrics_config = health_metrics_config or "DISABLED"
+        self.status = "AVAILABLE"
+
+    def update(
+        self,
+        ttl: int,
+        api_caching_behavior: str,
+        type: str,
+        health_metrics_config: Optional[str] = None,
+    ) -> None:
+        self.ttl = ttl
+        self.api_caching_behavior = api_caching_behavior
+        self.type = type
+        if health_metrics_config is not None:
+            self.health_metrics_config = health_metrics_config
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "ttl": self.ttl,
+            "transitEncryptionEnabled": self.transit_encryption_enabled,
+            "atRestEncryptionEnabled": self.at_rest_encryption_enabled,
+            "apiCachingBehavior": self.api_caching_behavior,
+            "type": self.type,
+            "healthMetricsConfig": self.health_metrics_config,
+            "status": self.status,
+        }
+
+
+# endregion
+
+
+# region: GraphqlAPI
 class GraphqlSchema(BaseModel):
     def __init__(self, definition: Any, region_name: str):
         self.definition = definition
@@ -137,49 +186,6 @@ class GraphqlAPIKey(BaseModel):
         }
 
 
-class APICache(BaseModel):
-    def __init__(
-        self,
-        ttl: int,
-        api_caching_behavior: str,
-        type_: str,
-        transit_encryption_enabled: Optional[bool] = None,
-        at_rest_encryption_enabled: Optional[bool] = None,
-        health_metrics_config: Optional[str] = None,
-    ):
-        self.ttl = ttl
-        self.api_caching_behavior = api_caching_behavior
-        self.type = type_
-        self.transit_encryption_enabled = transit_encryption_enabled or False
-        self.at_rest_encryption_enabled = at_rest_encryption_enabled or False
-        self.health_metrics_config = health_metrics_config or "DISABLED"
-        self.status = "AVAILABLE"
-
-    def update(
-        self,
-        ttl: int,
-        api_caching_behavior: str,
-        type: str,
-        health_metrics_config: Optional[str] = None,
-    ) -> None:
-        self.ttl = ttl
-        self.api_caching_behavior = api_caching_behavior
-        self.type = type
-        if health_metrics_config is not None:
-            self.health_metrics_config = health_metrics_config
-
-    def to_json(self) -> Dict[str, Any]:
-        return {
-            "ttl": self.ttl,
-            "transitEncryptionEnabled": self.transit_encryption_enabled,
-            "atRestEncryptionEnabled": self.at_rest_encryption_enabled,
-            "apiCachingBehavior": self.api_caching_behavior,
-            "type": self.type,
-            "healthMetricsConfig": self.health_metrics_config,
-            "status": self.status,
-        }
-
-
 class GraphqlAPI(BaseModel):
     def __init__(
         self,
@@ -327,12 +333,167 @@ class GraphqlAPI(BaseModel):
         }
 
 
+# endregion
+
+
+# region: EventsAPI
+class EventsAPIKey(BaseModel):
+    def __init__(self, description: str, expires: Optional[int]):
+        self.key_id = str(mock_random.uuid4())[0:6]
+        self.description = description
+        if not expires:
+            default_expiry = datetime.now(timezone.utc)
+            default_expiry = default_expiry.replace(
+                minute=0, second=0, microsecond=0, tzinfo=None
+            )
+            default_expiry = default_expiry + timedelta(days=7)
+            self.expires = unix_time(default_expiry)
+        else:
+            self.expires = expires
+
+    def update(self, description: Optional[str], expires: Optional[int]) -> None:
+        if description:
+            self.description = description
+        if expires:
+            self.expires = expires
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "id": self.key_id,
+            "description": self.description,
+            "expires": self.expires,
+            "deletes": self.expires,
+        }
+
+
+class ChannelNamespace(BaseModel):
+    def __init__(
+        self,
+        api_id: str,
+        name: str,
+        subscribe_auth_modes: List[Dict[str, str]],
+        publish_auth_modes: List[Dict[str, str]],
+        code_handlers: Optional[List[Dict[str, Any]]] = None,
+        handler_configs: Optional[Dict[str, Any]] = None,
+        account_id: str = "",
+        region: str = "",
+        backend: Optional["AppSyncBackend"] = None,
+    ) -> None:
+        self.api_id = api_id
+        self.name = name
+        self.subscribe_auth_modes = subscribe_auth_modes
+        self.publish_auth_modes = publish_auth_modes
+        self.code_handlers = code_handlers or []
+        self.handler_configs = handler_configs or {}
+
+        self.channel_namespace_arn = f"arn:{get_partition(region)}:appsync:{region}:{account_id}:apis/{api_id}/channelNamespace/{name}"
+
+        now = datetime.now(timezone.utc).isoformat()
+        self.created = now
+        self.last_modified = now
+
+        self.backend = backend
+
+    def to_json(self) -> Dict[str, Any]:
+        response = {
+            "apiId": self.api_id,
+            "name": self.name,
+            "subscribeAuthModes": self.subscribe_auth_modes,
+            "publishAuthModes": self.publish_auth_modes,
+            "channelNamespaceArn": self.channel_namespace_arn,
+            "created": self.created,
+            "lastModified": self.last_modified,
+            "handlerConfigs": self.handler_configs,
+        }
+
+        if self.code_handlers:
+            response["codeHandlers"] = self.code_handlers
+
+        if self.backend:
+            response["tags"] = self.backend.list_tags_for_resource(
+                self.channel_namespace_arn
+            )
+
+        return response
+
+
+class EventsAPI(BaseModel):
+    def __init__(
+        self,
+        account_id: str,
+        region: str,
+        name: str,
+        owner_contact: Optional[str],
+        event_config: Optional[Dict[str, Any]],
+        backend: "AppSyncBackend",
+    ) -> None:
+        self.region = region
+        self.name = name
+        self.api_id = str(mock_random.get_random_string(length=26))
+        self.owner_contact = owner_contact
+        self.event_config = event_config
+
+        self.api_arn = f"arn:{get_partition(self.region)}:appsync:{self.region}:{account_id}:apis/{self.api_id}"
+
+        self.api_keys: Dict[str, EventsAPIKey] = dict()
+        self.channel_namespaces: List[ChannelNamespace] = list()
+
+        dns_prefix = str(mock_random.get_random_string(length=26))
+        self.dns = {
+            "REALTIME": f"{dns_prefix}.appsync-realtime-api.{self.region}.amazonaws.com",
+            "HTTP": f"{dns_prefix}.appsync-api.{self.region}.amazonaws.com",
+        }
+
+        self.created = datetime.now(timezone.utc).isoformat()
+
+        self.backend = backend
+
+    def to_json(self) -> Dict[str, Any]:
+        response = {
+            "apiId": self.api_id,
+            "name": self.name,
+            "tags": self.backend.list_tags_for_resource(self.api_arn),
+            "dns": self.dns,
+            "apiArn": self.api_arn,
+            "created": self.created,
+            "eventConfig": self.event_config or {},  # Default to empty dict if None
+        }
+
+        if self.owner_contact:
+            response["ownerContact"] = self.owner_contact
+
+        return response
+
+    def create_api_key(self, description: str, expires: Optional[int]) -> EventsAPIKey:
+        api_key = EventsAPIKey(description, expires)
+        self.api_keys[api_key.key_id] = api_key
+        return api_key
+
+    def list_api_keys(self) -> Iterable[EventsAPIKey]:
+        return self.api_keys.values()
+
+    def delete_api_key(self, api_key_id: str) -> None:
+        self.api_keys.pop(api_key_id)
+
+    def update_api_key(
+        self, api_key_id: str, description: str, expires: Optional[int]
+    ) -> EventsAPIKey:
+        api_key = self.api_keys[api_key_id]
+        api_key.update(description, expires)
+        return api_key
+
+
+# endregion
+
+
+# region: AppSyncBackend
 class AppSyncBackend(BaseBackend):
     """Implementation of AppSync APIs."""
 
     def __init__(self, region_name: str, account_id: str) -> None:
         super().__init__(region_name, account_id)
         self.graphql_apis: Dict[str, GraphqlAPI] = dict()
+        self.events_apis: Dict[str, EventsAPI] = dict()
         self.tagger = TaggingService()
 
     def create_graphql_api(
@@ -418,18 +579,28 @@ class AppSyncBackend(BaseBackend):
 
     def create_api_key(
         self, api_id: str, description: str, expires: Optional[int]
-    ) -> GraphqlAPIKey:
-        return self.graphql_apis[api_id].create_api_key(description, expires)
+    ) -> Union[GraphqlAPIKey, EventsAPIKey]:
+        if api_id in self.graphql_apis:
+            return self.graphql_apis[api_id].create_api_key(description, expires)
+        else:
+            return self.events_apis[api_id].create_api_key(description, expires)
 
     def delete_api_key(self, api_id: str, api_key_id: str) -> None:
-        self.graphql_apis[api_id].delete_api_key(api_key_id)
+        if api_id in self.graphql_apis:
+            self.graphql_apis[api_id].delete_api_key(api_key_id)
+        else:
+            self.events_apis[api_id].delete_api_key(api_key_id)
 
-    def list_api_keys(self, api_id: str) -> Iterable[GraphqlAPIKey]:
+    def list_api_keys(
+        self, api_id: str
+    ) -> Iterable[Union[GraphqlAPIKey, EventsAPIKey]]:
         """
         Pagination or the maxResults-parameter have not yet been implemented.
         """
         if api_id in self.graphql_apis:
             return self.graphql_apis[api_id].list_api_keys()
+        elif api_id in self.events_apis:
+            return self.events_apis[api_id].list_api_keys()
         else:
             return []
 
@@ -439,10 +610,15 @@ class AppSyncBackend(BaseBackend):
         api_key_id: str,
         description: str,
         expires: Optional[int],
-    ) -> GraphqlAPIKey:
-        return self.graphql_apis[api_id].update_api_key(
-            api_key_id, description, expires
-        )
+    ) -> Union[GraphqlAPIKey, EventsAPIKey]:
+        if api_id in self.graphql_apis:
+            return self.graphql_apis[api_id].update_api_key(
+                api_key_id, description, expires
+            )
+        else:
+            return self.events_apis[api_id].update_api_key(
+                api_key_id, description, expires
+            )
 
     def start_schema_creation(self, api_id: str, definition: str) -> str:
         self.graphql_apis[api_id].start_schema_creation(definition)
@@ -531,5 +707,97 @@ class AppSyncBackend(BaseBackend):
             raise GraphqlAPICacheNotFound("flush")
         return
 
+    def create_api(
+        self,
+        name: str,
+        owner_contact: Optional[str],
+        tags: Optional[Dict[str, str]],
+        event_config: Optional[Dict[str, Any]],
+    ) -> EventsAPI:
+        events_api = EventsAPI(
+            account_id=self.account_id,
+            region=self.region_name,
+            name=name,
+            owner_contact=owner_contact,
+            event_config=event_config,
+            backend=self,
+        )
+
+        self.events_apis[events_api.api_id] = events_api
+
+        self.tagger.tag_resource(
+            events_api.api_arn, TaggingService.convert_dict_to_tags_input(tags)
+        )
+
+        return events_api
+
+    def list_apis(self) -> Iterable[EventsAPI]:
+        """
+        Pagination or the maxResults-parameter have not yet been implemented.
+        """
+        return self.events_apis.values()
+
+    def delete_api(self, api_id: str) -> None:
+        self.events_apis.pop(api_id)
+
+    def create_channel_namespace(
+        self,
+        api_id: str,
+        name: str,
+        subscribe_auth_modes: List[Dict[str, str]],
+        publish_auth_modes: List[Dict[str, str]],
+        code_handlers: Optional[List[Dict[str, Any]]] = None,
+        tags: Optional[Dict[str, str]] = None,
+        handler_configs: Optional[Dict[str, Any]] = None,
+    ) -> ChannelNamespace:
+        # Check if API exists
+        if api_id not in self.events_apis:
+            raise EventsAPINotFound(api_id)
+
+        channel_namespace = ChannelNamespace(
+            api_id=api_id,
+            name=name,
+            subscribe_auth_modes=subscribe_auth_modes,
+            publish_auth_modes=publish_auth_modes,
+            code_handlers=code_handlers,
+            handler_configs=handler_configs,
+            account_id=self.account_id,
+            region=self.region_name,
+            backend=self,
+        )
+
+        for api in self.events_apis.values():
+            if api.api_id == api_id:
+                api.channel_namespaces.append(channel_namespace)
+
+        if tags:
+            self.tagger.tag_resource(
+                channel_namespace.channel_namespace_arn,
+                TaggingService.convert_dict_to_tags_input(tags),
+            )
+
+        return channel_namespace
+
+    def list_channel_namespaces(self, api_id: str) -> Iterable[ChannelNamespace]:
+        if api_id not in self.events_apis:
+            raise EventsAPINotFound(api_id)
+        return self.events_apis[api_id].channel_namespaces
+
+    def delete_channel_namespace(self, api_id: str, name: str) -> None:
+        if api_id not in self.events_apis:
+            raise EventsAPINotFound(api_id)
+        for channel_namespace in self.events_apis[api_id].channel_namespaces:
+            if channel_namespace.name == name:
+                self.events_apis[api_id].channel_namespaces.remove(channel_namespace)
+                return
+
+    def get_api(self, api_id: str) -> EventsAPI:
+        if api_id not in self.events_apis:
+            raise EventsAPINotFound(api_id)
+        return self.events_apis[api_id]
+
+
+# endregion
+
 
 appsync_backends = BackendDict(AppSyncBackend, "appsync")
diff --git a/moto/appsync/responses.py b/moto/appsync/responses.py
index 962511bad..217d68525 100644
--- a/moto/appsync/responses.py
+++ b/moto/appsync/responses.py
@@ -1,10 +1,16 @@
 """Handles incoming appsync requests, invokes methods, returns responses."""
 
 import json
+import re
+from typing import Any
 from urllib.parse import unquote
+from uuid import uuid4
 
+from moto.core.common_types import TYPE_RESPONSE
 from moto.core.responses import BaseResponse
+from moto.core.utils import unix_time
 
+from .exceptions import ApiKeyValidityOutOfBoundsException, AWSValidationException
 from .models import AppSyncBackend, appsync_backends
 
 
@@ -14,6 +20,16 @@ class AppSyncResponse(BaseResponse):
     def __init__(self) -> None:
         super().__init__(service_name="appsync")
 
+    @staticmethod
+    def dns_event_response(request: Any, url: str, headers: Any) -> TYPE_RESPONSE:  # type: ignore[misc]
+        data = json.loads(request.data.decode("utf-8"))
+
+        response: dict[str, list[Any]] = {"failed": [], "successful": []}
+        for idx in range(len(data.get("events", []))):
+            response["successful"].append({"identifier": str(uuid4()), "index": idx})
+
+        return 200, {}, json.dumps(response).encode("utf-8")
+
     @property
     def appsync_backend(self) -> AppSyncBackend:
         """Return backend instance specific for this region."""
@@ -100,6 +116,15 @@ class AppSyncResponse(BaseResponse):
         api_id = self.path.split("/")[-2]
         description = params.get("description")
         expires = params.get("expires")
+
+        if expires:
+            current_time = int(unix_time())
+            min_validity = current_time + 86400  # 1 day in seconds
+            if expires < min_validity:
+                raise ApiKeyValidityOutOfBoundsException(
+                    "API key must be valid for a minimum of 1 days."
+                )
+
         api_key = self.appsync_backend.create_api_key(
             api_id=api_id, description=description, expires=expires
         )
@@ -123,6 +148,16 @@ class AppSyncResponse(BaseResponse):
         params = json.loads(self.body)
         description = params.get("description")
         expires = params.get("expires")
+
+        # Validate that API key expires at least 1 day from now
+        if expires:
+            current_time = int(unix_time())
+            min_validity = current_time + 86400  # 1 day in seconds
+            if expires < min_validity:
+                raise ApiKeyValidityOutOfBoundsException(
+                    "API key must be valid for a minimum of 1 days."
+                )
+
         api_key = self.appsync_backend.update_api_key(
             api_id=api_id,
             api_key_id=api_key_id,
@@ -251,3 +286,104 @@ class AppSyncResponse(BaseResponse):
             api_id=api_id,
         )
         return "{}"
+
+    def create_api(self) -> str:
+        params = json.loads(self.body)
+        name = params.get("name")
+
+        if name:
+            pattern = r"^[A-Za-z0-9_\-\ ]+$"
+            if not re.match(pattern, name):
+                raise AWSValidationException(
+                    "1 validation error detected: "
+                    "Value at 'name' failed to satisfy constraint: "
+                    "Member must satisfy regular expression pattern: "
+                    "[A-Za-z0-9_\\-\\ ]+"
+                )
+
+        owner_contact = params.get("ownerContact")
+        tags = params.get("tags", {})
+        event_config = params.get("eventConfig")
+
+        api = self.appsync_backend.create_api(
+            name=name,
+            owner_contact=owner_contact,
+            tags=tags,
+            event_config=event_config,
+        )
+
+        response = api.to_json()
+        return json.dumps({"api": response})
+
+    def list_apis(self) -> str:
+        apis = self.appsync_backend.list_apis()
+        return json.dumps(dict(apis=[api.to_json() for api in apis]))
+
+    def delete_api(self) -> str:
+        api_id = self.path.split("/")[-1]
+        self.appsync_backend.delete_api(api_id=api_id)
+        return "{}"
+
+    def create_channel_namespace(self) -> str:
+        params = json.loads(self.body)
+        api_id = self.path.split("/")[-2]
+        name = params.get("name")
+
+        if name:
+            pattern = r"^[A-Za-z0-9](?:[A-Za-z0-9\-]{0,48}[A-Za-z0-9])?$"
+            if not re.match(pattern, name):
+                raise AWSValidationException(
+                    "1 validation error detected: "
+                    "Value at 'name' failed to satisfy constraint: "
+                    "Member must satisfy regular expression pattern: "
+                    "([A-Za-z0-9](?:[A-Za-z0-9\\-]{0,48}[A-Za-z0-9])?)"
+                )
+
+        subscribe_auth_modes = params.get("subscribeAuthModes")
+        publish_auth_modes = params.get("publishAuthModes")
+        code_handlers = params.get("codeHandlers")
+        tags = params.get("tags", {})
+        handler_configs = params.get("handlerConfigs", {})
+
+        channel_namespace = self.appsync_backend.create_channel_namespace(
+            api_id=api_id,
+            name=name,
+            subscribe_auth_modes=subscribe_auth_modes,
+            publish_auth_modes=publish_auth_modes,
+            code_handlers=code_handlers,
+            tags=tags,
+            handler_configs=handler_configs,
+        )
+
+        return json.dumps({"channelNamespace": channel_namespace.to_json()})
+
+    def list_channel_namespaces(self) -> str:
+        api_id = self.path.split("/")[-2]
+        channel_namespaces = self.appsync_backend.list_channel_namespaces(api_id=api_id)
+        return json.dumps(
+            dict(
+                channelNamespaces=[
+                    channel_namespace.to_json()
+                    for channel_namespace in channel_namespaces
+                ]
+            )
+        )
+
+    def delete_channel_namespace(self) -> str:
+        path_parts = self.path.split("/")
+        api_id = path_parts[-3]
+        name = path_parts[-1]
+
+        self.appsync_backend.delete_channel_namespace(
+            api_id=api_id,
+            name=name,
+        )
+        return "{}"
+
+    def get_api(self) -> str:
+        api_id = self.path.split("/")[-1]
+
+        api = self.appsync_backend.get_api(api_id=api_id)
+        response = api.to_json()
+        response["tags"] = self.appsync_backend.list_tags_for_resource(api.api_arn)
+        return json.dumps(dict(api=response))
diff --git a/moto/appsync/urls.py b/moto/appsync/urls.py
index b1d43d7a9..a9faeceeb 100644
--- a/moto/appsync/urls.py
+++ b/moto/appsync/urls.py
@@ -4,6 +4,7 @@ from .responses import AppSyncResponse
 
 url_bases = [
     r"https?://appsync\.(.+)\.amazonaws\.com",
+    r"https?://([a-zA-Z0-9\-_]+)\.appsync-api\.(.+)\.amazonaws\.com",
 ]
 
 
@@ -20,4 +21,9 @@ url_paths = {
     "{0}/v1/apis/(?P<apiId>.*)/ApiCaches$": AppSyncResponse.dispatch,
     "{0}/v1/apis/(?P<apiId>.*)/ApiCaches/update$": AppSyncResponse.dispatch,
     "{0}/v1/apis/(?P<apiId>.*)/FlushCache$": AppSyncResponse.dispatch,
+    "{0}/v2/apis$": AppSyncResponse.dispatch,
+    "{0}/v2/apis/(?P<apiId>[^/]+)$": AppSyncResponse.dispatch,
+    "{0}/v2/apis/(?P<apiId>[^/]+)/channelNamespaces$": AppSyncResponse.dispatch,
+    "{0}/v2/apis/(?P<apiId>[^/]+)/channelNamespaces/(?P<name>[^/]+)$": AppSyncResponse.dispatch,
+    "{0}/event$": AppSyncResponse.dns_event_response,
 }
diff --git a/moto/backend_index.py b/moto/backend_index.py
index 4ce527433..b14ab59fd 100644
--- a/moto/backend_index.py
+++ b/moto/backend_index.py
@@ -17,6 +17,12 @@ backend_url_patterns = [
     ),
     ("appmesh", re.compile("https?://appmesh\\.(.+)\\.amazonaws\\.com")),
     ("appsync", re.compile("https?://appsync\\.(.+)\\.amazonaws\\.com")),
+    (
+        "appsync",
+        re.compile(
+            "https?://([a-zA-Z0-9\\-_]+)\\.appsync-api\\.(.+)\\.amazonaws\\.com"
+        ),
+    ),
     ("athena", re.compile("https?://athena\\.(.+)\\.amazonaws\\.com")),
     ("autoscaling", re.compile("https?://autoscaling\\.(.+)\\.amazonaws\\.com")),
     ("awslambda", re.compile("https?://lambda\\.(.+)\\.amazonaws\\.com")),
diff --git a/moto/codedeploy/models.py b/moto/codedeploy/models.py
index 80199a9e0..5412fff41 100644
--- a/moto/codedeploy/models.py
+++ b/moto/codedeploy/models.py
@@ -117,6 +117,7 @@ class DeploymentGroup(BaseModel):
     def to_dict(self) -> Dict[str, Any]:
         return {
             "applicationName": self.application.application_name,
+            "deploymentGroupId": self.deployment_group_id,
             "deploymentGroupName": self.deployment_group_name,
             "deploymentConfigName": str(self.deployment_config_name),
             "ec2TagFilters": self.ec2_tag_filters,
@@ -507,7 +508,7 @@ class CodeDeployBackend(BaseBackend):
             return []
 
         return [
-            deployment_group.deployment_group_id
+            deployment_group.deployment_group_name
             for deployment_group in self.deployment_groups[application_name].values()
         ]
 


Test patch:
diff --git a/other_langs/tests_go/dynamodbstreams.go b/other_langs/tests_go/dynamodbstreams.go
deleted file mode 100644
index c27a45af2..000000000
--- a/other_langs/tests_go/dynamodbstreams.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package main
-
-import (
-	"context"
-	"github.com/aws/aws-sdk-go-v2/aws"
-	"github.com/aws/aws-sdk-go-v2/config"
-	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
-	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
-	"github.com/aws/aws-sdk-go-v2/service/dynamodbstreams"
-)
-
-const (
-	Endpoint  = "http://localhost:5000"
-	Region    = "us-east-1"
-	TableName = "TableWithStreamEnabled"
-)
-
-func DescribeStream() *dynamodbstreams.DescribeStreamOutput {
-
-	cfg, err := config.LoadDefaultConfig(context.TODO())
-	if err != nil {
-		panic(err)
-	}
-
-	ddbClient := dynamodb.NewFromConfig(cfg, func(o *dynamodb.Options) {
-		o.BaseEndpoint = aws.String(Endpoint)
-		o.Region = Region
-	})
-
-	result, err := ddbClient.CreateTable(context.TODO(), &dynamodb.CreateTableInput{
-		TableName: aws.String(TableName),
-		AttributeDefinitions: []types.AttributeDefinition{
-			{
-				AttributeName: aws.String("PK"),
-				AttributeType: types.ScalarAttributeTypeS,
-			},
-			{
-				AttributeName: aws.String("SK"),
-				AttributeType: types.ScalarAttributeTypeS,
-			},
-		},
-		KeySchema: []types.KeySchemaElement{
-			{
-				AttributeName: aws.String("PK"),
-				KeyType:       types.KeyTypeHash,
-			},
-			{
-				AttributeName: aws.String("SK"),
-				KeyType:       types.KeyTypeRange,
-			},
-		},
-		ProvisionedThroughput: &types.ProvisionedThroughput{
-			ReadCapacityUnits:  aws.Int64(5),
-			WriteCapacityUnits: aws.Int64(5),
-		},
-		StreamSpecification: &types.StreamSpecification{
-			StreamEnabled:  aws.Bool(true),
-			StreamViewType: "NEW_AND_OLD_IMAGES",
-		},
-	})
-
-	if err != nil {
-		panic(err)
-	}
-
-	streamArn := *result.TableDescription.LatestStreamArn
-	ddbsClient := dynamodbstreams.NewFromConfig(cfg, func(o *dynamodbstreams.Options) {
-		o.BaseEndpoint = aws.String(Endpoint)
-		o.Region = Region
-	})
-	streamInfo, err := ddbsClient.DescribeStream(context.TODO(), &dynamodbstreams.DescribeStreamInput{
-		StreamArn: aws.String(streamArn),
-	})
-	if err != nil {
-		panic(err)
-	}
-	return streamInfo
-}
diff --git a/other_langs/tests_go/dynamodbstreams_test.go b/other_langs/tests_go/dynamodbstreams_test.go
index 80543de2e..f99c3b7d8 100644
--- a/other_langs/tests_go/dynamodbstreams_test.go
+++ b/other_langs/tests_go/dynamodbstreams_test.go
@@ -1,17 +1,85 @@
 package main
 
 import (
-	"fmt"
+	"context"
+	"github.com/aws/aws-sdk-go-v2/aws"
+	"github.com/aws/aws-sdk-go-v2/config"
+	"github.com/aws/aws-sdk-go-v2/credentials"
+	"github.com/aws/aws-sdk-go-v2/service/dynamodb"
+	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
+	"github.com/aws/aws-sdk-go-v2/service/dynamodbstreams"
 	"strings"
 	"testing"
 )
 
+const (
+	TableName = "TableWithStreamEnabled"
+)
+
 func TestDescribeStream(t *testing.T) {
 
-	result := DescribeStream()
-	streamArn := *result.StreamDescription.StreamArn
+	cfg, err := config.LoadDefaultConfig(
+		context.Background(),
+		config.WithBaseEndpoint("http://localhost:5000"),
+		config.WithRegion("us-east-1"),
+		config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("EXAMPLE", "EXAMPLE", "EXAMPLE")),
+		config.WithClientLogMode(aws.LogRequestWithBody|aws.LogResponseWithBody),
+	)
+
+	ddbClient := dynamodb.NewFromConfig(cfg)
+
+	CreateTableInput := &dynamodb.CreateTableInput{
+		TableName: aws.String(TableName),
+		AttributeDefinitions: []types.AttributeDefinition{
+			{
+				AttributeName: aws.String("PK"),
+				AttributeType: types.ScalarAttributeTypeS,
+			},
+			{
+				AttributeName: aws.String("SK"),
+				AttributeType: types.ScalarAttributeTypeS,
+			},
+		},
+		KeySchema: []types.KeySchemaElement{
+			{
+				AttributeName: aws.String("PK"),
+				KeyType:       types.KeyTypeHash,
+			},
+			{
+				AttributeName: aws.String("SK"),
+				KeyType:       types.KeyTypeRange,
+			},
+		},
+		ProvisionedThroughput: &types.ProvisionedThroughput{
+			ReadCapacityUnits:  aws.Int64(5),
+			WriteCapacityUnits: aws.Int64(5),
+		},
+		StreamSpecification: &types.StreamSpecification{
+			StreamEnabled:  aws.Bool(true),
+			StreamViewType: "NEW_AND_OLD_IMAGES",
+		},
+	}
+
+	ctx := context.Background()
+	result, err := ddbClient.CreateTable(ctx, CreateTableInput)
+	if err != nil {
+		panic(err)
+	}
+	streamArn := *result.TableDescription.LatestStreamArn
+
+	ddbsClient := dynamodbstreams.NewFromConfig(cfg)
+
+	DescribeStreamInput := &dynamodbstreams.DescribeStreamInput{
+		StreamArn: aws.String(streamArn),
+	}
+
+	streamInfo, err := ddbsClient.DescribeStream(ctx, DescribeStreamInput)
+	if err != nil {
+		panic(err)
+	}
+
+	streamArn = *streamInfo.StreamDescription.StreamArn
 	checkFor := TableName + "/stream/"
-	fmt.Println(streamArn)
 	if strings.Contains(streamArn, checkFor) == false {
 		t.Errorf("StreamArn does not contain our table name [%s].", TableName)
 	}
diff --git a/tests/test_appsync/test_appsync.py b/tests/test_appsync/test_appsync.py
index e34f62aba..a01f2658e 100644
--- a/tests/test_appsync/test_appsync.py
+++ b/tests/test_appsync/test_appsync.py
@@ -1,9 +1,15 @@
+import json
+from datetime import datetime, timedelta
+from unittest import SkipTest
+
 import boto3
 import pytest
+import requests
 from botocore.exceptions import ClientError
 
-from moto import mock_aws
+from moto import mock_aws, settings
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+from moto.core.utils import unix_time
 
 # 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
@@ -470,3 +476,376 @@ def test_flush_api_cache():
         type="T2_SMALL",
     )["apiCache"]
     client.flush_api_cache(apiId=api_id)
+
+
+@mock_aws
+def test_create_api():
+    client = boto3.client("appsync", region_name="eu-west-1")
+    resp = client.create_api(
+        name="api1",
+        eventConfig={
+            "authProviders": [
+                {"authType": "API_KEY"},
+            ],
+            "connectionAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+            "defaultPublishAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+            "defaultSubscribeAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+        },
+    )
+
+    assert "api" in resp
+    api = resp["api"]
+    assert api["name"] == "api1"
+    assert "apiId" in api
+    assert (
+        api["apiArn"] == f"arn:aws:appsync:eu-west-1:{ACCOUNT_ID}:apis/{api['apiId']}"
+    )
+    assert "dns" in api
+    assert "REALTIME" in api["dns"]
+    assert "HTTP" in api["dns"]
+    assert api["dns"]["REALTIME"].endswith(
+        ".appsync-realtime-api.eu-west-1.amazonaws.com"
+    )
+    assert api["dns"]["HTTP"].endswith(".appsync-api.eu-west-1.amazonaws.com")
+    assert "created" in api
+    assert api["eventConfig"]["authProviders"][0]["authType"] == "API_KEY"
+    assert api["tags"] == {}
+
+
+@mock_aws
+def test_create_api_with_all_params():
+    client = boto3.client("appsync", region_name="us-east-2")
+
+    event_config = {
+        "authProviders": [
+            {
+                "authType": "AWS_IAM",
+            },
+        ],
+        "connectionAuthModes": [{"authType": "AWS_IAM"}],
+        "defaultPublishAuthModes": [{"authType": "AWS_IAM"}],
+        "defaultSubscribeAuthModes": [{"authType": "API_KEY"}],
+        "logConfig": {
+            "logLevel": "ALL",
+            "cloudWatchLogsRoleArn": "arn:aws:iam::123456789012:role/MyRole",
+        },
+    }
+
+    resp = client.create_api(
+        name="MyEventsAPI",
+        ownerContact="owner@example.com",
+        tags={"key1": "value1", "key2": "value2"},
+        eventConfig=event_config,
+    )
+
+    api = resp["api"]
+
+    assert api["name"] == "MyEventsAPI"
+    assert api["ownerContact"] == "owner@example.com"
+    assert api["tags"] == {"key1": "value1", "key2": "value2"}
+    assert api["eventConfig"] == event_config
+    assert api["eventConfig"]["authProviders"][0]["authType"] == "AWS_IAM"
+    assert api["eventConfig"]["defaultPublishAuthModes"][0]["authType"] == "AWS_IAM"
+    assert api["eventConfig"]["defaultSubscribeAuthModes"][0]["authType"] == "API_KEY"
+    assert api["eventConfig"]["logConfig"]["logLevel"] == "ALL"
+
+
+@mock_aws
+def test_delete_api():
+    client = boto3.client("appsync", region_name="ap-southeast-1")
+
+    resp = client.create_api(
+        name="api1",
+        eventConfig={
+            "authProviders": [
+                {"authType": "API_KEY"},
+            ],
+            "connectionAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+            "defaultPublishAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+            "defaultSubscribeAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+        },
+    )
+    api_id = resp["api"]["apiId"]
+
+    resp = client.list_apis()
+    assert len(resp["apis"]) == 1
+
+    client.delete_api(apiId=api_id)
+
+    resp = client.list_apis()
+    assert len(resp["apis"]) == 0
+
+
+@mock_aws
+def test_create_channel_namespace():
+    client = boto3.client("appsync", region_name="us-east-2")
+    resp = client.create_api(
+        name="api1",
+        eventConfig={
+            "authProviders": [
+                {"authType": "API_KEY"},
+            ],
+            "connectionAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+            "defaultPublishAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+            "defaultSubscribeAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+        },
+    )
+    api_id = resp["api"]["apiId"]
+
+    channel_response = client.create_channel_namespace(
+        apiId=api_id,
+        name="testChannel",
+        subscribeAuthModes=[{"authType": "API_KEY"}],
+        publishAuthModes=[{"authType": "API_KEY"}],
+        tags={"key1": "value1", "key2": "value2"},
+        handlerConfigs={},
+    )
+
+    assert "channelNamespace" in channel_response
+
+    channel = channel_response["channelNamespace"]
+    assert channel["apiId"] == api_id
+    assert channel["name"] == "testChannel"
+    assert channel["subscribeAuthModes"] == [{"authType": "API_KEY"}]
+    assert channel["publishAuthModes"] == [{"authType": "API_KEY"}]
+    assert channel["tags"] == {"key1": "value1", "key2": "value2"}
+    assert "channelNamespaceArn" in channel
+    assert channel["channelNamespaceArn"].endswith(
+        f":apis/{api_id}/channelNamespace/testChannel"
+    )
+    assert "created" in channel
+    assert "lastModified" in channel
+    assert "handlerConfigs" in channel
+
+
+@mock_aws
+def test_delete_channel_namespace():
+    client = boto3.client("appsync", region_name="us-east-2")
+    resp = client.create_api(
+        name="api1",
+        eventConfig={
+            "authProviders": [
+                {"authType": "API_KEY"},
+            ],
+            "connectionAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+            "defaultPublishAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+            "defaultSubscribeAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+        },
+    )
+    api_id = resp["api"]["apiId"]
+
+    resp = client.create_channel_namespace(
+        apiId=api_id,
+        name="testChannel",
+        subscribeAuthModes=[{"authType": "API_KEY"}],
+        publishAuthModes=[{"authType": "API_KEY"}],
+    )
+
+    assert len(client.list_channel_namespaces(apiId=api_id)["channelNamespaces"]) == 1
+
+    client.delete_channel_namespace(apiId=api_id, name="testChannel")
+
+    assert len(client.list_channel_namespaces(apiId=api_id)["channelNamespaces"]) == 0
+
+
+@mock_aws
+def test_get_api():
+    client = boto3.client("appsync", region_name="ap-southeast-1")
+    api_id = client.create_api(
+        name="api1",
+        eventConfig={
+            "authProviders": [{"authType": "API_KEY"}],
+            "connectionAuthModes": [{"authType": "API_KEY"}],
+            "defaultPublishAuthModes": [{"authType": "API_KEY"}],
+            "defaultSubscribeAuthModes": [{"authType": "API_KEY"}],
+        },
+    )["api"]["apiId"]
+
+    resp = client.get_api(apiId=api_id)
+    assert "api" in resp
+    api = resp["api"]
+    assert api["name"] == "api1"
+    assert api["apiId"] == api_id
+
+
+@mock_aws
+def test_events_api_direct_http_request_e2e():
+    if not settings.TEST_DECORATOR_MODE:
+        raise SkipTest("Can only test this flow in DecoratorMode")
+    client = boto3.client("appsync", region_name="us-east-1")
+    api_resp = client.create_api(
+        name="events-api",
+        eventConfig={
+            "authProviders": [
+                {"authType": "API_KEY"},
+            ],
+            "connectionAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+            "defaultPublishAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+            "defaultSubscribeAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+        },
+    )
+    api_id = api_resp["api"]["apiId"]
+    api_http_url = api_resp["api"]["dns"]["HTTP"]
+
+    channel_name = "test-channel"
+    client.create_channel_namespace(
+        apiId=api_id,
+        name=channel_name,
+        subscribeAuthModes=[{"authType": "API_KEY"}],
+        publishAuthModes=[{"authType": "API_KEY"}],
+    )
+
+    three_days_ahead = datetime.now() + timedelta(days=3)
+    three_days_ahead_in_secs = int(unix_time(three_days_ahead))
+    key_resp = client.create_api_key(
+        apiId=api_id, description="test api key", expires=three_days_ahead_in_secs
+    )
+    api_key = key_resp["apiKey"]["id"]
+
+    foo_content = "test-foo-123"
+    bar_content = "test-bar-456"
+    event_payload = {
+        "channel": channel_name,
+        "events": [json.dumps({"foo": foo_content, "bar": bar_content}, default=str)],
+    }
+
+    endpoint_url = f"https://{api_http_url}/event"
+
+    response = requests.post(
+        endpoint_url,
+        headers={"Content-Type": "application/json", "x-api-key": api_key},
+        json=event_payload,
+        timeout=10,
+    )
+    assert response.status_code == 200
+    assert response.json()["failed"] == []
+    assert len(response.json()["successful"]) == 1
+
+    get_api_resp = client.get_api(apiId=api_id)
+    assert get_api_resp["api"]["apiId"] == api_id
+    assert get_api_resp["api"]["dns"]["HTTP"] == api_http_url
+
+    list_channels_resp = client.list_channel_namespaces(apiId=api_id)
+    assert len(list_channels_resp["channelNamespaces"]) == 1
+    assert list_channels_resp["channelNamespaces"][0]["name"] == channel_name
+
+    list_keys_resp = client.list_api_keys(apiId=api_id)
+    assert len(list_keys_resp["apiKeys"]) == 1
+    assert list_keys_resp["apiKeys"][0]["id"] == api_key
+
+
+@mock_aws
+def test_create_channel_namespace_invalid_name():
+    client = boto3.client("appsync", region_name="us-east-2")
+    resp = client.create_api(
+        name="api1",
+        eventConfig={
+            "authProviders": [
+                {"authType": "API_KEY"},
+            ],
+            "connectionAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+            "defaultPublishAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+            "defaultSubscribeAuthModes": [
+                {"authType": "API_KEY"},
+            ],
+        },
+    )
+    api_id = resp["api"]["apiId"]
+
+    with pytest.raises(ClientError) as exc:
+        client.create_channel_namespace(
+            apiId=api_id,
+            name="test-channel/",
+            subscribeAuthModes=[{"authType": "API_KEY"}],
+            publishAuthModes=[{"authType": "API_KEY"}],
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+    assert "1 validation error detected" in err["Message"]
+    assert "Value at 'name' failed to satisfy constraint" in err["Message"]
+    assert "Member must satisfy regular expression pattern" in err["Message"]
+
+    with pytest.raises(ClientError) as exc:
+        client.create_channel_namespace(
+            apiId=api_id,
+            name="-test-channel",
+            subscribeAuthModes=[{"authType": "API_KEY"}],
+            publishAuthModes=[{"authType": "API_KEY"}],
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+
+    with pytest.raises(ClientError) as exc:
+        client.create_channel_namespace(
+            apiId=api_id,
+            name="test-channel-",
+            subscribeAuthModes=[{"authType": "API_KEY"}],
+            publishAuthModes=[{"authType": "API_KEY"}],
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+
+
+@mock_aws
+def test_create_api_invalid_name():
+    client = boto3.client("appsync", region_name="us-east-2")
+
+    with pytest.raises(ClientError) as exc:
+        client.create_api(
+            name="invalid/api",
+            eventConfig={
+                "authProviders": [{"authType": "API_KEY"}],
+                "connectionAuthModes": [{"authType": "API_KEY"}],
+                "defaultPublishAuthModes": [{"authType": "API_KEY"}],
+                "defaultSubscribeAuthModes": [{"authType": "API_KEY"}],
+            },
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+
+    with pytest.raises(ClientError) as exc:
+        client.create_api(
+            name="invalid@api",
+            eventConfig={
+                "authProviders": [{"authType": "API_KEY"}],
+                "connectionAuthModes": [{"authType": "API_KEY"}],
+                "defaultPublishAuthModes": [{"authType": "API_KEY"}],
+                "defaultSubscribeAuthModes": [{"authType": "API_KEY"}],
+            },
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
diff --git a/tests/test_appsync/test_appsync_apikeys.py b/tests/test_appsync/test_appsync_apikeys.py
index bde5e922c..c0885a80e 100644
--- a/tests/test_appsync/test_appsync_apikeys.py
+++ b/tests/test_appsync/test_appsync_apikeys.py
@@ -1,6 +1,8 @@
 from datetime import datetime, timedelta
 
 import boto3
+import pytest
+from botocore.exceptions import ClientError
 
 from moto import mock_aws
 from moto.core.utils import unix_time
@@ -27,14 +29,14 @@ def test_create_api_key_simple():
 @mock_aws
 def test_create_api_key():
     client = boto3.client("appsync", region_name="ap-southeast-1")
-    tomorrow = datetime.now() + timedelta(days=1)
-    tomorrow_in_secs = int(unix_time(tomorrow))
+    two_days_ahead = datetime.now() + timedelta(days=2)
+    two_days_ahead_in_secs = int(unix_time(two_days_ahead))
 
     api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
         "graphqlApi"
     ]["apiId"]
     resp = client.create_api_key(
-        apiId=api_id, description="my first api key", expires=tomorrow_in_secs
+        apiId=api_id, description="my first api key", expires=two_days_ahead_in_secs
     )
 
     assert "apiKey" in resp
@@ -42,8 +44,8 @@ def test_create_api_key():
 
     assert "id" in api_key
     assert api_key["description"] == "my first api key"
-    assert api_key["expires"] == tomorrow_in_secs
-    assert api_key["deletes"] == tomorrow_in_secs
+    assert api_key["expires"] == two_days_ahead_in_secs
+    assert api_key["deletes"] == two_days_ahead_in_secs
 
 
 @mock_aws
@@ -111,3 +113,55 @@ def test_update_api_key():
     assert updated["description"] == "my second api key"
     assert updated["expires"] == original["expires"]
     assert updated["deletes"] == original["deletes"]
+
+
+@mock_aws
+def test_create_api_key_invalid_expiration():
+    client = boto3.client("appsync", region_name="us-east-1")
+
+    api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
+        "graphqlApi"
+    ]["apiId"]
+
+    twelve_hours_ahead = datetime.now() + timedelta(hours=12)
+    twelve_hours_ahead_in_secs = int(unix_time(twelve_hours_ahead))
+
+    with pytest.raises(ClientError) as exc:
+        client.create_api_key(
+            apiId=api_id,
+            description="invalid api key",
+            expires=twelve_hours_ahead_in_secs,
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ApiKeyValidityOutOfBoundsException"
+    assert "API key must be valid for a minimum of 1 days" in err["Message"]
+
+    yesterday = datetime.now() - timedelta(days=1)
+    yesterday_in_secs = int(unix_time(yesterday))
+
+    with pytest.raises(ClientError) as exc:
+        client.create_api_key(
+            apiId=api_id, description="invalid api key", expires=yesterday_in_secs
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ApiKeyValidityOutOfBoundsException"
+
+
+@mock_aws
+def test_create_api_key_expire_time_in_past():
+    client = boto3.client("appsync", region_name="us-east-1")
+
+    api_id = client.create_graphql_api(name="api1", authenticationType="API_KEY")[
+        "graphqlApi"
+    ]["apiId"]
+
+    yesterday = datetime.now() - timedelta(days=1)
+    yesterday_in_secs = int(unix_time(yesterday))
+
+    with pytest.raises(ClientError) as exc:
+        client.create_api_key(
+            apiId=api_id, description="invalid api key", expires=yesterday_in_secs
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ApiKeyValidityOutOfBoundsException"
+    assert "API key must be valid for a minimum of 1 days" in err["Message"]
diff --git a/tests/test_codedeploy/test_codedeploy.py b/tests/test_codedeploy/test_codedeploy.py
index a35fa52d6..ce41ab003 100644
--- a/tests/test_codedeploy/test_codedeploy.py
+++ b/tests/test_codedeploy/test_codedeploy.py
@@ -326,12 +326,15 @@ def test_get_deployment_group():
     client.create_application(
         applicationName=application_name, computePlatform="Lambda"
     )
-    client.create_deployment_group(
+
+    response = client.create_deployment_group(
         applicationName=application_name,
         deploymentGroupName=deployment_group_name,
         serviceRoleArn=service_role_arn,
     )
 
+    deployment_group_id = response["deploymentGroupId"]
+
     response = client.get_deployment_group(
         applicationName=application_name, deploymentGroupName=deployment_group_name
     )
@@ -340,6 +343,7 @@ def test_get_deployment_group():
 
     # required fields
     assert deployment_group_info["applicationName"] == application_name
+    assert deployment_group_info["deploymentGroupId"] == deployment_group_id
     assert deployment_group_info["deploymentGroupName"] == deployment_group_name
     assert deployment_group_info["serviceRoleArn"] == service_role_arn
 
@@ -554,6 +558,11 @@ def test_list_deployment_groups():
     resp = client.list_deployment_groups(applicationName=application_name)
     assert len(resp["deploymentGroups"]) == 2
 
+    # ensure both deployment group names are in the response
+    assert set([deployment_group_name, deployment_group_name2]) == set(
+        resp["deploymentGroups"]
+    )
+
 
 @mock_aws
 def test_application_tagging():
@@ -584,7 +593,7 @@ def test_deployment_group_tagging():
     client.create_application(applicationName=app_name, computePlatform="Server")
 
     initial_tags = [{"Key": "Team", "Value": "Platform"}]
-    response = client.create_deployment_group(
+    client.create_deployment_group(
         applicationName=app_name,
         deploymentGroupName=dg_name,
         serviceRoleArn=service_role_arn,