Repository: getmoto/moto
Pull request #8360 (2024-12-03 15:08:54)

directconnect: MACsec associate/disassociate + LAG create/describe
* Additional features for Direct Connect
  * LAG create (`aws directconnect create-lag`)
  * LAGs describe (`aws directconnect describe-lags`)
  * MACsec associate (`aws directconnect associate-mac-sec-key`)
  * MACsec disassociate (`aws directconnect disassociate-mac-sec-key`)
* Corrected minor variable typo (co**nnn**ection_id vs co**nn**ection_id)
* Tests added to cover new functionality
* Linter checks passed locally
* Tests passed locally


Notes
* Scaffolding tool (scripts/scaffold.py) was used to generate code stubs
* MACsec associate/disassociate works for both "normal" connections and LAGs.

ptgeorge: Fixed (hopefully) python 3.8 lint issue.  The change works fine with 3.11 (don't have 3.8 readily available)
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8360?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
Attention: Patch coverage is `94.92754%` with `7 lines` in your changes missing coverage. Please review.
> Project coverage is 94.58%. Comparing base [(`bf49e22`)](https://app.codecov.io/gh/getmoto/moto/commit/bf49e22f41be7444dc20e05dfa5dd13f25672eba?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`c87b185`)](https://app.codecov.io/gh/getmoto/moto/commit/c87b18509b3cbef79e101de7a64cc7395dcca87f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 51 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/getmoto/moto/pull/8360?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Patch % | Lines |
|---|---|---|
| [moto/directconnect/models.py](https://app.codecov.io/gh/getmoto/moto/pull/8360?src=pr&el=tree&filepath=moto%2Fdirectconnect%2Fmodels.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9kaXJlY3Rjb25uZWN0L21vZGVscy5weQ==) | 94.50% | [5 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/8360?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) |
| [moto/directconnect/exceptions.py](https://app.codecov.io/gh/getmoto/moto/pull/8360?src=pr&el=tree&filepath=moto%2Fdirectconnect%2Fexceptions.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9kaXJlY3Rjb25uZWN0L2V4Y2VwdGlvbnMucHk=) | 71.42% | [2 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/8360?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #8360      +/-   ##
==========================================
+ Coverage   94.56%   94.58%   +0.02%     
==========================================
  Files        1158     1159       +1     
  Lines      100744   101275     +531     
==========================================
+ Hits        95267    95794     +527     
- Misses       5477     5481       +4     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8360/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/8360/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.86% <0.00%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8360/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.56% <94.92%> (+0.02%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/8360?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).

ptgeorge: Checks seem to be ephemeral failures and also unrelated to the changes.
ptgeorge: Merged `main` in yesterday.  Github workflows haven't ran yet...

Patch:
diff --git a/moto/athena/models.py b/moto/athena/models.py
index 988cd387a..c9cb498c1 100644
--- a/moto/athena/models.py
+++ b/moto/athena/models.py
@@ -107,7 +107,7 @@ class Execution(BaseModel):
         query: str,
         context: str,
         config: Dict[str, Any],
-        workgroup: WorkGroup,
+        workgroup: Optional[WorkGroup],
         execution_parameters: Optional[List[str]],
     ):
         self.id = str(mock_random.uuid4())
@@ -246,14 +246,14 @@ class AthenaBackend(BaseBackend):
         query: str,
         context: str,
         config: Dict[str, Any],
-        workgroup: WorkGroup,
+        workgroup: str,
         execution_parameters: Optional[List[str]],
     ) -> str:
         execution = Execution(
             query=query,
             context=context,
             config=config,
-            workgroup=workgroup,
+            workgroup=self.work_groups.get(workgroup),
             execution_parameters=execution_parameters,
         )
         self.executions[execution.id] = execution
@@ -271,7 +271,13 @@ class AthenaBackend(BaseBackend):
     def get_query_execution(self, exec_id: str) -> Execution:
         return self.executions[exec_id]
 
-    def list_query_executions(self) -> Dict[str, Execution]:
+    def list_query_executions(self, workgroup: Optional[str]) -> Dict[str, Execution]:
+        if workgroup is not None:
+            return {
+                exec_id: execution
+                for exec_id, execution in self.executions.items()
+                if execution.workgroup and execution.workgroup.name == workgroup
+            }
         return self.executions
 
     def get_query_results(self, exec_id: str) -> QueryResults:
diff --git a/moto/athena/responses.py b/moto/athena/responses.py
index 938f9a9ea..23b074160 100644
--- a/moto/athena/responses.py
+++ b/moto/athena/responses.py
@@ -104,7 +104,8 @@ class AthenaResponse(BaseResponse):
         return json.dumps(result.to_dict())
 
     def list_query_executions(self) -> str:
-        executions = self.athena_backend.list_query_executions()
+        workgroup = self._get_param("WorkGroup")
+        executions = self.athena_backend.list_query_executions(workgroup)
         return json.dumps({"QueryExecutionIds": [i for i in executions.keys()]})
 
     def stop_query_execution(self) -> str:
diff --git a/moto/directconnect/enums.py b/moto/directconnect/enums.py
index becce7976..adb441bf3 100644
--- a/moto/directconnect/enums.py
+++ b/moto/directconnect/enums.py
@@ -13,6 +13,16 @@ class ConnectionStateType(str, Enum):
     UNKNOWN = "unknown"
 
 
+class LagStateType(str, Enum):
+    AVAILABLE = "available"
+    DELETED = "deleted"
+    DELETING = "deleting"
+    DOWN = "down"
+    PENDING = "pending"
+    REQUESTED = "requested"
+    UNKNOWN = "unknown"
+
+
 class PortEncryptionStatusType(str, Enum):
     UP = "Encryption Up"
     DOWN = "Encryption Down"
diff --git a/moto/directconnect/exceptions.py b/moto/directconnect/exceptions.py
index ff418b037..d34425b5e 100644
--- a/moto/directconnect/exceptions.py
+++ b/moto/directconnect/exceptions.py
@@ -13,8 +13,24 @@ class ConnectionIdMissing(DXConnectionError):
 
 
 class ConnectionNotFound(DXConnectionError):
-    def __init__(self, connnection_id: str, region: str) -> None:
+    def __init__(self, connection_id: str, region: str) -> None:
         super().__init__(
             "ConnectionNotFound",
-            f"{connnection_id} does not match any connections in region {region}.",
+            f"{connection_id} does not match any connections in region {region}.",
+        )
+
+
+class LAGNotFound(DXConnectionError):
+    def __init__(self, lag_id: str, region: str) -> None:
+        super().__init__(
+            "LagNotFound",
+            f"{lag_id} does not match any LAGs in region {region}.",
+        )
+
+
+class MacSecKeyNotFound(DXConnectionError):
+    def __init__(self, secret_arn: str, connection_id: str) -> None:
+        super().__init__(
+            "MacSecKeyNotFound",
+            f"Secret ARN '{secret_arn}' not found for connection {connection_id}.",
         )
diff --git a/moto/directconnect/models.py b/moto/directconnect/models.py
index 74b888a3b..0ef31cbde 100644
--- a/moto/directconnect/models.py
+++ b/moto/directconnect/models.py
@@ -2,7 +2,7 @@
 
 from dataclasses import dataclass, field
 from datetime import datetime
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, List, Optional, Tuple
 
 from moto.core.base_backend import BackendDict, BaseBackend
 from moto.core.common_models import BaseModel
@@ -10,23 +10,30 @@ from moto.core.common_models import BaseModel
 from .enums import (
     ConnectionStateType,
     EncryptionModeType,
+    LagStateType,
     MacSecKeyStateType,
     PortEncryptionStatusType,
 )
-from .exceptions import ConnectionIdMissing, ConnectionNotFound
+from .exceptions import (
+    ConnectionIdMissing,
+    ConnectionNotFound,
+    LAGNotFound,
+    MacSecKeyNotFound,
+)
 
 
 @dataclass
 class MacSecKey(BaseModel):
-    secret_arn: str
-    ckn: str
+    secret_arn: Optional[str]
+    ckn: Optional[str]
     state: MacSecKeyStateType
     start_on: str
+    cak: Optional[str] = None
 
     def to_dict(self) -> Dict[str, str]:
         return {
-            "secretARN": self.secret_arn,
-            "ckn": self.ckn,
+            "secretARN": self.secret_arn or "",
+            "ckn": self.ckn or "",
             "state": self.state,
             "startOn": self.start_on,
         }
@@ -89,12 +96,68 @@ class Connection(BaseModel):
         }
 
 
+@dataclass
+class LAG(BaseModel):
+    aws_device_v2: str
+    aws_device: str
+    aws_logical_device_id: str
+    connections_bandwidth: str
+    number_of_connections: int
+    minimum_links: int
+    connections: List[Connection]
+    lag_name: str
+    lag_state: LagStateType
+    encryption_mode: EncryptionModeType
+    has_logical_redundancy: bool
+    jumbo_frame_capable: bool
+    location: str
+    mac_sec_capable: Optional[bool]
+    mac_sec_keys: List[MacSecKey]
+    owner_account: str
+    provider_name: Optional[str]
+    region: str
+    tags: Optional[List[Dict[str, str]]]
+    lag_id: str = field(default="", init=False)
+
+    def __post_init__(self) -> None:
+        if self.lag_id == "":
+            self.lag_id = (
+                f"dxlag-moto-{self.lag_name}-{datetime.now().strftime('%Y%m%d%H%M%S')}"
+            )
+
+    def to_dict(
+        self,
+    ) -> Dict[str, Any]:
+        return {
+            "awsDevice": self.aws_device,
+            "awsDeviceV2": self.aws_device_v2,
+            "awsLogicalDeviceId": self.aws_logical_device_id,
+            "connectionsBandwidth": self.connections_bandwidth,
+            "numberOfConnections": self.number_of_connections,
+            "minimumLinks": self.minimum_links,
+            "connections": [conn.to_dict() for conn in self.connections],
+            "lagId": self.lag_id,
+            "lagName": self.lag_name,
+            "lagState": self.lag_state,
+            "encryptionMode": self.encryption_mode,
+            "hasLogicalRedundancy": self.has_logical_redundancy,
+            "jumboFrameCapable": self.jumbo_frame_capable,
+            "location": self.location,
+            "macSecCapable": self.mac_sec_capable,
+            "macSecKeys": [key.to_dict() for key in self.mac_sec_keys],
+            "providerName": self.provider_name,
+            "region": self.region,
+            "tags": self.tags,
+        }
+
+
 class DirectConnectBackend(BaseBackend):
     """Implementation of DirectConnect APIs."""
 
     def __init__(self, region_name: str, account_id: str) -> None:
         super().__init__(region_name, account_id)
         self.connections: Dict[str, Connection] = {}
+        self.lags: Dict[str, LAG] = {}
 
     def describe_connections(self, connection_id: Optional[str]) -> List[Connection]:
         if connection_id and connection_id not in self.connections:
@@ -180,5 +243,148 @@ class DirectConnectBackend(BaseBackend):
             return connection
         raise ConnectionNotFound(connection_id, self.region_name)
 
+    def associate_mac_sec_key(
+        self,
+        connection_id: str,
+        secret_arn: Optional[str],
+        ckn: Optional[str],
+        cak: Optional[str],
+    ) -> Tuple[str, List[MacSecKey]]:
+        mac_sec_key = MacSecKey(
+            secret_arn=secret_arn or "mock_secret_arn",
+            ckn=ckn,
+            cak=cak,
+            state=MacSecKeyStateType.ASSOCIATED,
+            start_on=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+        )
+
+        if connection_id.startswith("dxlag-"):
+            return self._associate_mac_sec_key_with_lag(
+                lag_id=connection_id, mac_sec_key=mac_sec_key
+            )
+
+        return self._associate_mac_sec_key_with_connection(
+            connection_id=connection_id, mac_sec_key=mac_sec_key
+        )
+
+    def _associate_mac_sec_key_with_lag(
+        self, lag_id: str, mac_sec_key: MacSecKey
+    ) -> Tuple[str, List[MacSecKey]]:
+        lag = self.lags.get(lag_id) or None
+        if not lag:
+            raise LAGNotFound(lag_id, self.region_name)
+
+        lag.mac_sec_keys.append(mac_sec_key)
+        for connection in lag.connections:
+            connection.mac_sec_keys = lag.mac_sec_keys
+
+        return lag_id, lag.mac_sec_keys
+
+    def _associate_mac_sec_key_with_connection(
+        self, connection_id: str, mac_sec_key: MacSecKey
+    ) -> Tuple[str, List[MacSecKey]]:
+        connection = self.connections.get(connection_id) or None
+        if not connection:
+            raise ConnectionNotFound(connection_id, self.region_name)
+
+        self.connections[connection_id].mac_sec_keys.append(mac_sec_key)
+        return connection_id, self.connections[connection_id].mac_sec_keys
+
+    def create_lag(
+        self,
+        number_of_connections: int,
+        location: str,
+        connections_bandwidth: str,
+        lag_name: str,
+        connection_id: Optional[str],
+        tags: Optional[List[Dict[str, str]]],
+        child_connection_tags: Optional[List[Dict[str, str]]],
+        provider_name: Optional[str],
+        request_mac_sec: Optional[bool],
+    ) -> LAG:
+        if connection_id:
+            raise NotImplementedError(
+                "creating a lag with a connection_id is not currently supported by moto"
+            )
+        encryption_mode = EncryptionModeType.NO
+        mac_sec_keys = []
+        if request_mac_sec:
+            encryption_mode = EncryptionModeType.MUST
+            mac_sec_keys = [
+                MacSecKey(
+                    secret_arn="mock_secret_arn",
+                    ckn="mock_ckn",
+                    state=MacSecKeyStateType.ASSOCIATED,
+                    start_on=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+                )
+            ]
+        lag = LAG(
+            aws_device_v2="mock_device_v2",
+            aws_device="mock_device",
+            aws_logical_device_id="mock_logical_device_id",
+            connections_bandwidth=connections_bandwidth,
+            lag_name=lag_name,
+            lag_state=LagStateType.AVAILABLE,
+            minimum_links=0,
+            encryption_mode=encryption_mode,
+            has_logical_redundancy=False,
+            jumbo_frame_capable=False,
+            number_of_connections=number_of_connections,
+            connections=[],
+            location=location,
+            mac_sec_capable=request_mac_sec,
+            mac_sec_keys=mac_sec_keys,
+            owner_account=self.account_id,
+            provider_name=provider_name,
+            region=self.region_name,
+            tags=tags,
+        )
+        for i in range(number_of_connections):
+            connection = self.create_connection(
+                location=location,
+                bandwidth=connections_bandwidth,
+                connection_name=f"Requested Connection {i+1} for Lag {lag.lag_id}",
+                lag_id=lag.lag_id,
+                tags=child_connection_tags,
+                request_mac_sec=False,
+                provider_name=provider_name,
+            )
+            if request_mac_sec:
+                connection.mac_sec_capable = True
+                connection.mac_sec_keys = mac_sec_keys
+                connection.encryption_mode = encryption_mode
+            lag.connections.append(connection)
+
+        self.lags[lag.lag_id] = lag
+        return lag
+
+    def describe_lags(self, lag_id: Optional[str]) -> List[LAG]:
+        if lag_id and lag_id not in self.lags:
+            raise LAGNotFound(lag_id, self.region_name)
+        if lag_id:
+            lag = self.lags.get(lag_id)
+            return [] if not lag else [lag]
+        return list(self.lags.values())
+
+    def disassociate_mac_sec_key(
+        self, connection_id: str, secret_arn: str
+    ) -> Tuple[str, MacSecKey]:
+        mac_sec_keys: List[MacSecKey] = []
+        if connection_id.startswith("dxlag-"):
+            if connection_id in self.lags:
+                mac_sec_keys = self.lags[connection_id].mac_sec_keys
+        elif connection_id in self.connections:
+            mac_sec_keys = self.connections[connection_id].mac_sec_keys
+        if not mac_sec_keys:
+            raise ConnectionNotFound(connection_id, self.region_name)
+
+        arn_casefold = secret_arn.casefold()
+        for i, mac_sec_key in enumerate(mac_sec_keys):
+            if str(mac_sec_key.secret_arn).casefold() == arn_casefold:
+                mac_sec_key.state = MacSecKeyStateType.DISASSOCIATED
+                return connection_id, mac_sec_keys.pop(i)
+
+        raise MacSecKeyNotFound(secret_arn=secret_arn, connection_id=connection_id)
+
 
 directconnect_backends = BackendDict(DirectConnectBackend, "directconnect")
diff --git a/moto/directconnect/responses.py b/moto/directconnect/responses.py
index d01946e3c..7e2960d6d 100644
--- a/moto/directconnect/responses.py
+++ b/moto/directconnect/responses.py
@@ -4,7 +4,7 @@ import json
 
 from moto.core.responses import BaseResponse
 
-from .models import Connection, DirectConnectBackend, directconnect_backends
+from .models import LAG, Connection, DirectConnectBackend, directconnect_backends
 
 
 class DirectConnectResponse(BaseResponse):
@@ -54,3 +54,70 @@ class DirectConnectResponse(BaseResponse):
             new_encryption_mode=params.get("encryptionMode"),
         )
         return json.dumps(connection.to_dict())
+
+    def associate_mac_sec_key(self) -> str:
+        params = json.loads(self.body)
+        connection_id = params.get("connectionId")
+        secret_arn = params.get("secretARN")
+        ckn = params.get("ckn")
+        cak = params.get("cak")
+        connection_id, mac_sec_keys = self.directconnect_backend.associate_mac_sec_key(
+            connection_id=connection_id,
+            secret_arn=secret_arn,
+            ckn=ckn,
+            cak=cak,
+        )
+        return json.dumps(
+            dict(
+                connectionId=connection_id,
+                macSecKeys=[mac_sec_key.to_dict() for mac_sec_key in mac_sec_keys],
+            )
+        )
+
+    def create_lag(self) -> str:
+        params = json.loads(self.body)
+        number_of_connections = params.get("numberOfConnections")
+        location = params.get("location")
+        connections_bandwidth = params.get("connectionsBandwidth")
+        lag_name = params.get("lagName")
+        connection_id = params.get("connectionId")
+        tags = params.get("tags")
+        child_connection_tags = params.get("childConnectionTags")
+        provider_name = params.get("providerName")
+        request_mac_sec = params.get("requestMACSec")
+        lag: LAG = self.directconnect_backend.create_lag(
+            number_of_connections=number_of_connections,
+            location=location,
+            connections_bandwidth=connections_bandwidth,
+            lag_name=lag_name,
+            connection_id=connection_id,
+            tags=tags,
+            child_connection_tags=child_connection_tags,
+            provider_name=provider_name,
+            request_mac_sec=request_mac_sec,
+        )
+        return json.dumps(lag.to_dict())
+
+    def describe_lags(self) -> str:
+        params = json.loads(self.body)
+        lags = self.directconnect_backend.describe_lags(
+            lag_id=params.get("lagId"),
+        )
+        return json.dumps(dict(lags=[lag.to_dict() for lag in lags]))
+
+    def disassociate_mac_sec_key(self) -> str:
+        params = json.loads(self.body)
+        connection_id = params.get("connectionId")
+        secret_arn = params.get("secretARN")
+        connection_id, mac_sec_key = (
+            self.directconnect_backend.disassociate_mac_sec_key(
+                connection_id=connection_id,
+                secret_arn=secret_arn,
+            )
+        )
+        return json.dumps(
+            dict(
+                connectionId=connection_id,
+                macSecKeys=[mac_sec_key.to_dict()],
+            )
+        )
diff --git a/moto/memorydb/models.py b/moto/memorydb/models.py
index 37396b589..e6fd366c1 100644
--- a/moto/memorydb/models.py
+++ b/moto/memorydb/models.py
@@ -369,7 +369,10 @@ class MemoryDBBackend(BaseBackend):
         return default_subnet_ids
 
     def _list_arns(self) -> List[str]:
-        return [cluster.arn for cluster in self.clusters.values()]
+        cluster_arns = [cluster.arn for cluster in self.clusters.values()]
+        snapshot_arns = [snapshot.arn for snapshot in self.snapshots.values()]
+        subnet_group_arns = [subnet.arn for subnet in self.subnet_groups.values()]
+        return cluster_arns + snapshot_arns + subnet_group_arns
 
     def create_cluster(
         self,
@@ -455,6 +458,8 @@ class MemoryDBBackend(BaseBackend):
             tags,
         )
         self.subnet_groups[subnet_group_name] = subnet_group
+        if tags:
+            self.tag_resource(subnet_group.arn, tags)
         return subnet_group
 
     def create_snapshot(
@@ -483,6 +488,8 @@ class MemoryDBBackend(BaseBackend):
             source=source,
         )
         self.snapshots[snapshot_name] = snapshot
+        if tags:
+            self.tag_resource(snapshot.arn, tags)
         return snapshot
 
     def describe_clusters(
@@ -561,16 +568,27 @@ class MemoryDBBackend(BaseBackend):
 
     def list_tags(self, resource_arn: str) -> List[Dict[str, str]]:
         if resource_arn not in self._list_arns():
-            cluster_name = resource_arn.split("/")[-1]
-            raise ClusterNotFoundFault(f"{cluster_name} is not present")
+            # Get the resource name from the resource_arn
+            resource_name = resource_arn.split("/")[-1]
+            if "subnetgroup" in resource_arn:
+                raise SubnetGroupNotFoundFault(f"{resource_name} is not present")
+            elif "snapshot" in resource_arn:
+                raise SnapshotNotFoundFault(f"{resource_name} is not present")
+            else:
+                raise ClusterNotFoundFault(f"{resource_name} is not present")
         return self.tagger.list_tags_for_resource(arn=resource_arn)["Tags"]
 
     def tag_resource(
         self, resource_arn: str, tags: List[Dict[str, str]]
     ) -> List[Dict[str, str]]:
         if resource_arn not in self._list_arns():
-            cluster_name = resource_arn.split("/")[-1]
-            raise ClusterNotFoundFault(f"{cluster_name} is not present")
+            resource_name = resource_arn.split("/")[-1]
+            if "subnetgroup" in resource_arn:
+                raise SubnetGroupNotFoundFault(f"{resource_name} is not present")
+            elif "snapshot" in resource_arn:
+                raise SnapshotNotFoundFault(f"{resource_name} is not present")
+            else:
+                raise ClusterNotFoundFault(f"{resource_name} is not present")
         self.tagger.tag_resource(resource_arn, tags)
         return self.tagger.list_tags_for_resource(arn=resource_arn)["Tags"]
 
@@ -578,8 +596,13 @@ class MemoryDBBackend(BaseBackend):
         self, resource_arn: str, tag_keys: List[str]
     ) -> List[Dict[str, str]]:
         if resource_arn not in self._list_arns():
-            cluster_name = resource_arn.split("/")[-1]
-            raise ClusterNotFoundFault(f"{cluster_name} is not present")
+            resource_name = resource_arn.split("/")[-1]
+            if "subnetgroup" in resource_arn:
+                raise SubnetGroupNotFoundFault(f"{resource_name} is not present")
+            elif "snapshot" in resource_arn:
+                raise SnapshotNotFoundFault(f"{resource_name} is not present")
+            else:
+                raise ClusterNotFoundFault(f"{resource_name} is not present")
         list_tags = self.list_tags(resource_arn=resource_arn)
         list_keys = [i["Key"] for i in list_tags]
         invalid_keys = [key for key in tag_keys if key not in list_keys]


Test patch:
diff --git a/tests/test_athena/test_athena.py b/tests/test_athena/test_athena.py
index c48c08f6a..e539c42b1 100644
--- a/tests/test_athena/test_athena.py
+++ b/tests/test_athena/test_athena.py
@@ -438,6 +438,50 @@ def test_list_query_executions():
     assert executions["QueryExecutionIds"][0] == exec_id
 
 
+@mock_aws
+def test_list_query_executions_by_workgroup():
+    client = boto3.client("athena", region_name="us-east-1")
+
+    create_basic_workgroup(client=client, name="athena_workgroup")
+    create_basic_workgroup(client=client, name="athena_workgroup_1")
+
+    client.start_query_execution(
+        QueryString="query1",
+        QueryExecutionContext={"Database": "string"},
+        ResultConfiguration={"OutputLocation": "string"},
+        WorkGroup="athena_workgroup",
+    )
+    exec_result = client.start_query_execution(
+        QueryString="query1",
+        QueryExecutionContext={"Database": "string"},
+        ResultConfiguration={"OutputLocation": "string"},
+        WorkGroup="athena_workgroup_1",
+    )
+    exec_id = exec_result["QueryExecutionId"]
+
+    executions = client.list_query_executions(WorkGroup="athena_workgroup_1")
+    assert len(executions["QueryExecutionIds"]) == 1
+    assert executions["QueryExecutionIds"][0] == exec_id
+
+
+@mock_aws
+def test_list_query_executions_by_workgroup_when_none_match():
+    client = boto3.client("athena", region_name="us-east-1")
+
+    create_basic_workgroup(client=client, name="athena_workgroup")
+    create_basic_workgroup(client=client, name="athena_workgroup_1")
+
+    client.start_query_execution(
+        QueryString="query1",
+        QueryExecutionContext={"Database": "string"},
+        ResultConfiguration={"OutputLocation": "string"},
+        WorkGroup="athena_workgroup",
+    )
+
+    executions = client.list_query_executions(WorkGroup="athena_workgroup_1")
+    assert len(executions["QueryExecutionIds"]) == 0
+
+
 @mock_aws
 def test_list_named_queries():
     client = boto3.client("athena", region_name="us-east-1")
diff --git a/tests/test_directconnect/test_directconnect.py b/tests/test_directconnect/test_directconnect.py
index 85cefb7ac..4b569d2a5 100644
--- a/tests/test_directconnect/test_directconnect.py
+++ b/tests/test_directconnect/test_directconnect.py
@@ -85,3 +85,132 @@ def test_update_connection(client):
         encryptionMode="should_encrypt",
     )
     assert connection["encryptionMode"] == "should_encrypt"
+
+
+def test_associate_mac_sec_key_connection(client):
+    connection = client.create_connection(
+        location="EqDC2",
+        bandwidth="10Gbps",
+        connectionName="TestConnection1",
+    )
+    resp = client.associate_mac_sec_key(
+        connectionId=connection["connectionId"], ckn="_fake_ckn_", cak="_fake_cak_"
+    )
+    assert resp["connectionId"] == connection["connectionId"]
+    mac_sec_keys = resp["macSecKeys"]
+    assert len(mac_sec_keys) == 1
+    assert mac_sec_keys[0]["ckn"] == "_fake_ckn_"
+    assert "cak" not in mac_sec_keys[0]
+    assert mac_sec_keys[0]["secretARN"] == "mock_secret_arn"
+
+
+def test_associate_mac_sec_key_lag(client):
+    lag = client.create_lag(
+        numberOfConnections=1,
+        location="eqDC2",
+        connectionsBandwidth="10Gbps",
+        lagName="TestLag1",
+    )
+    resp = client.associate_mac_sec_key(
+        connectionId=lag["lagId"], ckn="_fake_ckn_", cak="_fake_cak_"
+    )
+    assert resp["connectionId"] == lag["lagId"]
+    mac_sec_keys = resp["macSecKeys"]
+    assert len(mac_sec_keys) == 1
+    assert mac_sec_keys[0]["ckn"] == "_fake_ckn_"
+    assert "cak" not in mac_sec_keys[0]
+    assert mac_sec_keys[0]["secretARN"] == "mock_secret_arn"
+
+
+def test_create_lag(client):
+    lag = client.create_lag(
+        numberOfConnections=1,
+        location="eqDC2",
+        connectionsBandwidth="10Gbps",
+        lagName="TestLag0",
+    )
+    assert lag["lagId"].startswith("dxlag-moto")
+    assert lag["lagState"] == "available"
+    assert len(lag["connections"]) == 1
+    connection = lag["connections"][0]
+    assert connection["connectionName"].startswith(
+        "Requested Connection 1 for Lag dxlag-moto"
+    )
+
+
+def test_describe_lags(client):
+    client.create_lag(
+        location="EqDC2",
+        connectionsBandwidth="10Gbps",
+        lagName="TestLag1",
+        numberOfConnections=1,
+        requestMACSec=False,
+    )
+    time.sleep(0.1)
+    client.create_lag(
+        location="EqDC2",
+        connectionsBandwidth="10Gbps",
+        lagName="TestLag2",
+        numberOfConnections=1,
+        requestMACSec=True,
+    )
+    time.sleep(0.1)
+    resp = client.describe_lags()
+    lags = resp["lags"]
+    assert len(lags) == 2
+    assert len(lags[0]["connections"]) == 1
+    assert len(lags[1]["connections"]) == 1
+    assert not lags[0]["macSecCapable"]
+    assert not lags[0]["connections"][0]["macSecCapable"]
+    assert lags[1]["macSecCapable"]
+    assert lags[1]["connections"][0]["macSecCapable"]
+    assert len(lags[0]["macSecKeys"]) == 0
+    assert len(lags[0]["connections"][0]["macSecKeys"]) == 0
+    assert len(lags[1]["macSecKeys"]) == 1
+    assert len(lags[1]["connections"][0]["macSecKeys"]) == 1
+    assert lags[0]["encryptionMode"] == "no_encrypt"
+    assert lags[0]["connections"][0]["encryptionMode"] == "no_encrypt"
+    assert lags[1]["encryptionMode"] == "must_encrypt"
+    assert lags[1]["connections"][0]["encryptionMode"] == "must_encrypt"
+    resp = client.describe_lags(lagId=lags[0]["lagId"])
+    assert len(resp["lags"]) == 1
+
+
+def _test_disassociate_mac_sec_key_common(client, connection_id: str):
+    secret_arn = "_fake_secret_arn_"
+    assoc_resp = client.associate_mac_sec_key(
+        connectionId=connection_id,
+        ckn="_fake_ckn_",
+        cak="_fake_cak_",
+        secretARN=secret_arn,
+    )
+    assert assoc_resp["connectionId"] == connection_id
+    resp = client.disassociate_mac_sec_key(
+        connectionId=connection_id, secretARN=secret_arn
+    )
+    assert resp["connectionId"] == connection_id
+    mac_sec_keys = resp["macSecKeys"]
+    assert len(mac_sec_keys) == 1
+    assert mac_sec_keys[0]["secretARN"] == secret_arn
+    assert mac_sec_keys[0]["state"] == "disassociated"
+
+
+def test_disassociate_mac_sec_key_connection(client):
+    connection = client.create_connection(
+        location="EqDC2",
+        bandwidth="10Gbps",
+        connectionName="TestConnection1",
+    )
+    connection_id = connection["connectionId"]
+    _test_disassociate_mac_sec_key_common(client=client, connection_id=connection_id)
+
+
+def test_disassociate_mac_sec_key_lag(client):
+    lag = client.create_lag(
+        numberOfConnections=1,
+        location="eqDC2",
+        connectionsBandwidth="10Gbps",
+        lagName="TestLag1",
+    )
+    lag_id = lag["lagId"]
+    _test_disassociate_mac_sec_key_common(client=client, connection_id=lag_id)
diff --git a/tests/test_memorydb/test_memorydb.py b/tests/test_memorydb/test_memorydb.py
index 8b4ff0c5c..6f20771cf 100644
--- a/tests/test_memorydb/test_memorydb.py
+++ b/tests/test_memorydb/test_memorydb.py
@@ -21,6 +21,9 @@ def create_subnet_group(client, region_name):
         SubnetGroupName="my_subnet_group",
         Description="This is my subnet group",
         SubnetIds=[subnet1.id, subnet2.id],
+        Tags=[
+            {"Key": "foo", "Value": "bar"},
+        ],
     )
     return subnet_group
 
@@ -395,6 +398,32 @@ def test_list_tags():
     assert "bar" in resp["TagList"][0]["Value"]
 
 
+@mock_aws
+def test_list_tags_snapshot():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    client.create_cluster(
+        ClusterName="test-memory-db", NodeType="db.t4g.small", ACLName="open-access"
+    )
+    snapshot = client.create_snapshot(
+        ClusterName="test-memory-db",
+        SnapshotName="my-snapshot",
+        Tags=[
+            {"Key": "foo1", "Value": "bar1"},
+        ],
+    )
+    resp = client.list_tags(ResourceArn=snapshot["Snapshot"]["ARN"])
+    assert len(resp["TagList"]) == 1
+
+
+@mock_aws
+def test_list_tags_subnetgroups():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    subnet_group = create_subnet_group(client, "us-east-2")
+    sg = subnet_group["SubnetGroup"]
+    resp = client.list_tags(ResourceArn=sg["ARN"])
+    assert len(resp["TagList"]) == 1
+
+
 @mock_aws
 def test_list_tags_invalid_cluster_fails():
     client = boto3.client("memorydb", region_name="us-east-2")
@@ -406,6 +435,28 @@ def test_list_tags_invalid_cluster_fails():
     assert err["Code"] == "ClusterNotFoundFault"
 
 
+@mock_aws
+def test_list_tags_invalid_snapshot_fails():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    with pytest.raises(ClientError) as ex:
+        client.list_tags(
+            ResourceArn=f"arn:aws:memorydb:us-east-1:{ACCOUNT_ID}:snapshot/foobar",
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "SnapshotNotFoundFault"
+
+
+@mock_aws
+def test_list_tags_invalid_subnetgroup_fails():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    with pytest.raises(ClientError) as ex:
+        client.list_tags(
+            ResourceArn=f"arn:aws:memorydb:us-east-1:{ACCOUNT_ID}:subnetgroup/foobar",
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "SubnetGroupNotFoundFault"
+
+
 @mock_aws
 def test_tag_resource():
     client = boto3.client("memorydb", region_name="us-east-2")
@@ -441,6 +492,30 @@ def test_tag_resource_invalid_cluster_fails():
     assert err["Code"] == "ClusterNotFoundFault"
 
 
+@mock_aws
+def test_tag_resource_invalid_snapshot_fails():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    with pytest.raises(ClientError) as ex:
+        client.tag_resource(
+            ResourceArn=f"arn:aws:memorydb:us-east-1:{ACCOUNT_ID}:snapshot/foobar",
+            Tags=[{"Key": "key2", "Value": "value2"}],
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "SnapshotNotFoundFault"
+
+
+@mock_aws
+def test_tag_resource_invalid_subnetgroup_fails():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    with pytest.raises(ClientError) as ex:
+        client.tag_resource(
+            ResourceArn=f"arn:aws:memorydb:us-east-1:{ACCOUNT_ID}:subnetgroup/foobar",
+            Tags=[{"Key": "key2", "Value": "value2"}],
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "SubnetGroupNotFoundFault"
+
+
 @mock_aws
 def test_untag_resource():
     client = boto3.client("memorydb", region_name="us-east-2")
@@ -474,6 +549,30 @@ def test_untag_resource_invalid_cluster_fails():
     assert err["Code"] == "ClusterNotFoundFault"
 
 
+@mock_aws
+def test_untag_resource_invalid_snapshot_fails():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    with pytest.raises(ClientError) as ex:
+        client.untag_resource(
+            ResourceArn=f"arn:aws:memorydb:us-east-1:{ACCOUNT_ID}:snapshot/foobar",
+            TagKeys=["key1"],
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "SnapshotNotFoundFault"
+
+
+@mock_aws
+def test_untag_resource_invalid_subnetgroup_fails():
+    client = boto3.client("memorydb", region_name="us-east-2")
+    with pytest.raises(ClientError) as ex:
+        client.untag_resource(
+            ResourceArn=f"arn:aws:memorydb:us-east-1:{ACCOUNT_ID}:subnetgroup/foobar",
+            TagKeys=["key1"],
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "SubnetGroupNotFoundFault"
+
+
 @mock_aws
 def test_untag_resource_invalid_keys_fails():
     client = boto3.client("memorydb", region_name="us-east-2")