Repository: getmoto/moto
Pull request #8366 (2024-12-05 15:06:51)

directconnect: doc update (coverage)
* Minor doc update related to directconnect ( forgotten in #8360 )

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8366?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.58%. Comparing base [(`549d4e2`)](https://app.codecov.io/gh/getmoto/moto/commit/549d4e2c5c565dadfdcf86d435aecf451c253cf9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`9516536`)](https://app.codecov.io/gh/getmoto/moto/commit/95165364d81de66a02b3bae5a0aa012b6183d6e7?dropdown=coverage&el=desc&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    #8366   +/-   ##
=======================================
  Coverage   94.58%   94.58%           
=======================================
  Files        1159     1159           
  Lines      101297   101297           
=======================================
+ Hits        95814    95816    +2     
+ Misses       5483     5481    -2     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8366/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/8366/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.86% <ø> (ø)` | |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8366/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.56% <ø> (+<0.01%)` | :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/8366?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).


Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 1ed94cd6e..650962035 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -1956,7 +1956,7 @@
 
 ## directconnect
 <details>
-<summary>6% implemented</summary>
+<summary>12% implemented</summary>
 
 - [ ] accept_direct_connect_gateway_association_proposal
 - [ ] allocate_connection_on_interconnect
@@ -1966,7 +1966,7 @@
 - [ ] allocate_transit_virtual_interface
 - [ ] associate_connection_with_lag
 - [ ] associate_hosted_connection
-- [ ] associate_mac_sec_key
+- [x] associate_mac_sec_key
 - [ ] associate_virtual_interface
 - [ ] confirm_connection
 - [ ] confirm_customer_agreement
@@ -1979,7 +1979,7 @@
 - [ ] create_direct_connect_gateway_association
 - [ ] create_direct_connect_gateway_association_proposal
 - [ ] create_interconnect
-- [ ] create_lag
+- [x] create_lag
 - [ ] create_private_virtual_interface
 - [ ] create_public_virtual_interface
 - [ ] create_transit_virtual_interface
@@ -2002,7 +2002,7 @@
 - [ ] describe_hosted_connections
 - [ ] describe_interconnect_loa
 - [ ] describe_interconnects
-- [ ] describe_lags
+- [x] describe_lags
 - [ ] describe_loa
 - [ ] describe_locations
 - [ ] describe_router_configuration
@@ -2010,7 +2010,7 @@
 - [ ] describe_virtual_gateways
 - [ ] describe_virtual_interfaces
 - [ ] disassociate_connection_from_lag
-- [ ] disassociate_mac_sec_key
+- [x] disassociate_mac_sec_key
 - [ ] list_virtual_interface_test_history
 - [ ] start_bgp_failover_test
 - [ ] stop_bgp_failover_test
diff --git a/docs/docs/services/directconnect.rst b/docs/docs/services/directconnect.rst
index 5339e3942..b0f2318e0 100644
--- a/docs/docs/services/directconnect.rst
+++ b/docs/docs/services/directconnect.rst
@@ -24,7 +24,7 @@ directconnect
 - [ ] allocate_transit_virtual_interface
 - [ ] associate_connection_with_lag
 - [ ] associate_hosted_connection
-- [ ] associate_mac_sec_key
+- [x] associate_mac_sec_key
 - [ ] associate_virtual_interface
 - [ ] confirm_connection
 - [ ] confirm_customer_agreement
@@ -37,7 +37,7 @@ directconnect
 - [ ] create_direct_connect_gateway_association
 - [ ] create_direct_connect_gateway_association_proposal
 - [ ] create_interconnect
-- [ ] create_lag
+- [x] create_lag
 - [ ] create_private_virtual_interface
 - [ ] create_public_virtual_interface
 - [ ] create_transit_virtual_interface
@@ -60,7 +60,7 @@ directconnect
 - [ ] describe_hosted_connections
 - [ ] describe_interconnect_loa
 - [ ] describe_interconnects
-- [ ] describe_lags
+- [x] describe_lags
 - [ ] describe_loa
 - [ ] describe_locations
 - [ ] describe_router_configuration
@@ -68,7 +68,7 @@ directconnect
 - [ ] describe_virtual_gateways
 - [ ] describe_virtual_interfaces
 - [ ] disassociate_connection_from_lag
-- [ ] disassociate_mac_sec_key
+- [x] disassociate_mac_sec_key
 - [ ] list_virtual_interface_test_history
 - [ ] start_bgp_failover_test
 - [ ] stop_bgp_failover_test
diff --git a/moto/rds/models.py b/moto/rds/models.py
index 51229c102..a65d69ad3 100644
--- a/moto/rds/models.py
+++ b/moto/rds/models.py
@@ -1838,15 +1838,19 @@ class RDSBackend(BaseBackend):
         source_snapshot_identifier: str,
         target_snapshot_identifier: str,
         tags: Optional[List[Dict[str, str]]] = None,
+        copy_tags: bool = False,
     ) -> DBSnapshot:
         if source_snapshot_identifier not in self.database_snapshots:
             raise DBSnapshotNotFoundError(source_snapshot_identifier)
 
         source_snapshot = self.database_snapshots[source_snapshot_identifier]
-        if tags is None:
-            tags = source_snapshot.tags
-        else:
-            tags = self._merge_tags(source_snapshot.tags, tags)
+
+        # When tags are passed, AWS does NOT copy/merge tags of the
+        # source snapshot, even when copy_tags=True is given.
+        # But when tags=[], AWS does honor copy_tags=True.
+        if not tags:
+            tags = source_snapshot.tags if copy_tags else []
+
         return self.create_db_snapshot(
             db_instance=source_snapshot.database,
             db_snapshot_identifier=target_snapshot_identifier,
diff --git a/moto/rds/responses.py b/moto/rds/responses.py
index 7b8911369..fabafe7fa 100644
--- a/moto/rds/responses.py
+++ b/moto/rds/responses.py
@@ -303,8 +303,9 @@ class RDSResponse(BaseResponse):
         source_snapshot_identifier = self._get_param("SourceDBSnapshotIdentifier")
         target_snapshot_identifier = self._get_param("TargetDBSnapshotIdentifier")
         tags = self.unpack_list_params("Tags", "Tag")
+        copy_tags = self._get_param("CopyTags")
         snapshot = self.backend.copy_db_snapshot(
-            source_snapshot_identifier, target_snapshot_identifier, tags
+            source_snapshot_identifier, target_snapshot_identifier, tags, copy_tags
         )
         template = self.response_template(COPY_SNAPSHOT_TEMPLATE)
         return template.render(snapshot=snapshot)


Test patch:
diff --git a/tests/test_rds/test_rds.py b/tests/test_rds/test_rds.py
index 68913255f..7da065ace 100644
--- a/tests/test_rds/test_rds.py
+++ b/tests/test_rds/test_rds.py
@@ -1076,6 +1076,55 @@ def test_copy_db_snapshots(delete_db_instance: bool):
     assert result["TagList"] == []
 
 
+original_snapshot_tags = [{"Key": "original", "Value": "snapshot tags"}]
+new_snapshot_tags = [{"Key": "new", "Value": "tag"}]
+
+
+@pytest.mark.parametrize(
+    "kwargs,expected_tags",
+    [
+        # No Tags parameter, CopyTags defaults to False -> no tags
+        ({}, []),
+        # No Tags parameter, CopyTags set to True -> use tags of original snapshot
+        ({"CopyTags": True}, original_snapshot_tags),
+        # When "Tags" are given, they become the only tags of the snapshot.
+        ({"Tags": new_snapshot_tags}, new_snapshot_tags),
+        # When "Tags" are given, they become the only tags of the snapshot. Even if CopyTags is True!
+        ({"Tags": new_snapshot_tags, "CopyTags": True}, new_snapshot_tags),
+        # When "Tags" are given but empty, CopyTags=True takes effect again!
+        ({"Tags": [], "CopyTags": True}, original_snapshot_tags),
+    ],
+    ids=(
+        "no_parameters",
+        "copytags_true",
+        "only_tags",
+        "copytags_true_and_tags",
+        "copytags_true_and_empty_tags",
+    ),
+)
+@mock_aws
+def test_copy_db_snapshots_copytags_and_tags(kwargs, expected_tags):
+    conn = boto3.client("rds", region_name=DEFAULT_REGION)
+    conn.create_db_instance(
+        DBInstanceIdentifier="db-primary-1",
+        Engine="postgres",
+        DBInstanceClass="db.m1.small",
+    )
+    conn.create_db_snapshot(
+        DBInstanceIdentifier="db-primary-1",
+        DBSnapshotIdentifier="snapshot",
+        Tags=original_snapshot_tags,
+    )
+
+    target_snapshot = conn.copy_db_snapshot(
+        SourceDBSnapshotIdentifier="snapshot",
+        TargetDBSnapshotIdentifier="snapshot-copy",
+        **kwargs,
+    ).get("DBSnapshot")
+    result = conn.list_tags_for_resource(ResourceName=target_snapshot["DBSnapshotArn"])
+    assert result["TagList"] == expected_tags
+
+
 @mock_aws
 def test_describe_db_snapshots():
     conn = boto3.client("rds", region_name=DEFAULT_REGION)