Repository: getmoto/moto
Pull request #8540 (2025-01-31 17:51:26)

RDS: add OriginalSnapshotCreateTime to DB snapshots
In contrast to `SnapshotCreateTime`, the `OriginalSnapshotCreateTime` does not change when the snapshot gets copied.

Testing uses only 3 decimals for timestamp because the `iso_8601_datetime_with_milliseconds()` only provides 3. Skip some tests when in server mode, because `freeze_time` does not work there.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8540?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 92.63%. Comparing base [(`024067f`)](https://app.codecov.io/gh/getmoto/moto/commit/024067f0b6f9083056d5ad2d35074c6525bfea4c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`67f1dd9`)](https://app.codecov.io/gh/getmoto/moto/commit/67f1dd9825883104eb44499943f6736e415f7249?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    #8540   +/-   ##
=======================================
  Coverage   92.63%   92.63%           
=======================================
  Files        1229     1229           
  Lines      106422   106423    +1     
=======================================
+ Hits        98580    98582    +2     
+ Misses       7842     7841    -1     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8540/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/8540/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `27.92% <0.00%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8540/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `92.60% <100.00%> (+<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/8540?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/moto/rds/models.py b/moto/rds/models.py
index 6f0aa2ce3..8656d9a82 100644
--- a/moto/rds/models.py
+++ b/moto/rds/models.py
@@ -942,6 +942,7 @@ class DBSnapshot(RDSBaseModel):
         snapshot_id: str,
         snapshot_type: str,
         tags: List[Dict[str, str]],
+        original_created_at: Optional[str] = None,
     ):
         super().__init__(backend)
         self.database = database
@@ -950,6 +951,7 @@ class DBSnapshot(RDSBaseModel):
         self.tags = tags
         self.status = "available"
         self.created_at = iso_8601_datetime_with_milliseconds()
+        self.original_created_at = original_created_at or self.created_at
         self.attributes: List[Dict[str, Any]] = []
 
     @property
@@ -960,6 +962,14 @@ class DBSnapshot(RDSBaseModel):
     def snapshot_arn(self) -> str:
         return self.arn
 
+    @property
+    def snapshot_create_time(self) -> str:
+        return self.created_at
+
+    @property
+    def original_snapshot_create_time(self) -> str:
+        return self.original_created_at
+
 
 class ExportTask(BaseModel):
     def __init__(
@@ -1314,6 +1324,7 @@ class RDSBackend(BaseBackend):
         db_snapshot_identifier: str,
         snapshot_type: str = "manual",
         tags: Optional[List[Dict[str, str]]] = None,
+        original_created_at: Optional[str] = None,
     ) -> DBSnapshot:
         if isinstance(db_instance, str):
             database = self.databases.get(db_instance)
@@ -1333,7 +1344,12 @@ class RDSBackend(BaseBackend):
         if database.copy_tags_to_snapshot and not tags:
             tags = database.get_tags()
         snapshot = DBSnapshot(
-            self, database, db_snapshot_identifier, snapshot_type, tags
+            self,
+            database,
+            db_snapshot_identifier,
+            snapshot_type,
+            tags,
+            original_created_at,
         )
         self.database_snapshots[db_snapshot_identifier] = snapshot
         return snapshot
@@ -1364,6 +1380,7 @@ class RDSBackend(BaseBackend):
             db_instance=source_snapshot.database,
             db_snapshot_identifier=target_snapshot_identifier,
             tags=tags,
+            original_created_at=source_snapshot.original_created_at,
         )
 
     def delete_db_snapshot(self, db_snapshot_identifier: str) -> DBSnapshot:


Test patch:
diff --git a/tests/test_rds/test_rds.py b/tests/test_rds/test_rds.py
index 254be6747..29b151cec 100644
--- a/tests/test_rds/test_rds.py
+++ b/tests/test_rds/test_rds.py
@@ -4,8 +4,10 @@ from uuid import uuid4
 import boto3
 import pytest
 from botocore.exceptions import ClientError
+from dateutil.tz import tzutc
+from freezegun import freeze_time
 
-from moto import mock_aws
+from moto import mock_aws, settings
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 from moto.rds.exceptions import InvalidDBInstanceIdentifier, InvalidDBSnapshotIdentifier
 from moto.rds.models import RDSBackend
@@ -757,13 +759,19 @@ def test_create_db_snapshots(client):
 
     create_db_instance(DBInstanceIdentifier="db-primary-1")
 
-    snapshot = client.create_db_snapshot(
-        DBInstanceIdentifier="db-primary-1", DBSnapshotIdentifier="g-1"
-    )["DBSnapshot"]
+    snapshot_create_time = datetime.datetime(2025, 1, 2, tzinfo=tzutc())
+    with freeze_time(snapshot_create_time):
+        snapshot = client.create_db_snapshot(
+            DBInstanceIdentifier="db-primary-1", DBSnapshotIdentifier="g-1"
+        )["DBSnapshot"]
 
     assert snapshot["Engine"] == "postgres"
     assert snapshot["DBInstanceIdentifier"] == "db-primary-1"
     assert snapshot["DBSnapshotIdentifier"] == "g-1"
+    if not settings.TEST_SERVER_MODE:
+        assert snapshot["SnapshotCreateTime"] == snapshot_create_time
+        assert snapshot["SnapshotCreateTime"] == snapshot["OriginalSnapshotCreateTime"]
+
     result = client.list_tags_for_resource(ResourceName=snapshot["DBSnapshotArn"])
     assert result["TagList"] == []
 
@@ -827,22 +835,30 @@ def test_copy_db_snapshots(
 ):
     create_db_instance(DBInstanceIdentifier="db-primary-1")
 
-    client.create_db_snapshot(
-        DBInstanceIdentifier="db-primary-1", DBSnapshotIdentifier="snapshot-1"
-    )
+    snapshot_create_time = datetime.datetime(2025, 1, 2, tzinfo=tzutc())
+    with freeze_time(snapshot_create_time):
+        client.create_db_snapshot(
+            DBInstanceIdentifier="db-primary-1", DBSnapshotIdentifier="snapshot-1"
+        )
 
     if delete_db_instance:
         # Delete the original instance, but the copy snapshot operation should still succeed.
         client.delete_db_instance(DBInstanceIdentifier="db-primary-1")
 
-    target_snapshot = client.copy_db_snapshot(
-        SourceDBSnapshotIdentifier=db_snapshot_identifier,
-        TargetDBSnapshotIdentifier="snapshot-2",
-    )["DBSnapshot"]
+    target_snapshot_create_time = snapshot_create_time + datetime.timedelta(minutes=1)
+    with freeze_time(target_snapshot_create_time):
+        target_snapshot = client.copy_db_snapshot(
+            SourceDBSnapshotIdentifier=db_snapshot_identifier,
+            TargetDBSnapshotIdentifier="snapshot-2",
+        )["DBSnapshot"]
 
     assert target_snapshot["Engine"] == "postgres"
     assert target_snapshot["DBInstanceIdentifier"] == "db-primary-1"
     assert target_snapshot["DBSnapshotIdentifier"] == "snapshot-2"
+    if not settings.TEST_SERVER_MODE:
+        assert target_snapshot["SnapshotCreateTime"] == target_snapshot_create_time
+        assert target_snapshot["OriginalSnapshotCreateTime"] == snapshot_create_time
+
     result = client.list_tags_for_resource(
         ResourceName=target_snapshot["DBSnapshotArn"]
     )