Repository: getmoto/moto
Pull request #6460 (2023-06-29 11:02:40)

Route53: Implement GetHealthCheckStatus
This PR adds support for GetHealthCheckStatus endpoint in Route53

<https://docs.aws.amazon.com/Route53/latest/APIReference/API_GetHealthCheckStatus.html>


Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index d2def447b..106d32f4c 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -5776,7 +5776,7 @@
 - [X] get_health_check
 - [ ] get_health_check_count
 - [ ] get_health_check_last_failure_reason
-- [ ] get_health_check_status
+- [X] get_health_check_status
 - [X] get_hosted_zone
 - [X] get_hosted_zone_count
 - [ ] get_hosted_zone_limit
@@ -7444,4 +7444,4 @@
 - workspaces
 - workspaces-web
 - xray
-</details>
\ No newline at end of file
+</details>
diff --git a/docs/docs/services/route53.rst b/docs/docs/services/route53.rst
index 148621fd2..60130848c 100644
--- a/docs/docs/services/route53.rst
+++ b/docs/docs/services/route53.rst
@@ -65,7 +65,7 @@ route53
 - [X] get_health_check
 - [ ] get_health_check_count
 - [ ] get_health_check_last_failure_reason
-- [ ] get_health_check_status
+- [X] get_health_check_status
 - [X] get_hosted_zone
 - [X] get_hosted_zone_count
 - [ ] get_hosted_zone_limit
diff --git a/moto/cloudfront/models.py b/moto/cloudfront/models.py
index 5f54f0cb9..18713890c 100644
--- a/moto/cloudfront/models.py
+++ b/moto/cloudfront/models.py
@@ -148,6 +148,8 @@ class DistributionConfig:
         self.aliases = ((config.get("Aliases") or {}).get("Items") or {}).get(
             "CNAME"
         ) or []
+        if isinstance(self.aliases, str):
+            self.aliases = [self.aliases]
         self.comment = config.get("Comment") or ""
         self.default_cache_behavior = DefaultCacheBehaviour(
             config["DefaultCacheBehavior"]
diff --git a/moto/route53/responses.py b/moto/route53/responses.py
index ad18f6dbd..647166fd1 100644
--- a/moto/route53/responses.py
+++ b/moto/route53/responses.py
@@ -1,4 +1,6 @@
 """Handles Route53 API requests, invokes method and returns response."""
+import datetime
+import re
 from urllib.parse import parse_qs, urlparse
 
 from jinja2 import Template
@@ -7,6 +9,7 @@ import xmltodict
 
 from moto.core.common_types import TYPE_RESPONSE
 from moto.core.responses import BaseResponse
+from moto.core.utils import iso_8601_datetime_with_milliseconds
 from moto.route53.exceptions import InvalidChangeBatch
 from moto.route53.models import route53_backends, Route53Backend
 
@@ -342,6 +345,31 @@ class Route53(BaseResponse):
             template = Template(UPDATE_HEALTH_CHECK_RESPONSE)
             return 200, headers, template.render(health_check=health_check)
 
+    def health_check_status_response(self, request: Any, full_url: str, headers: Any) -> TYPE_RESPONSE:  # type: ignore[return]
+        self.setup_class(request, full_url, headers)
+
+        parsed_url = urlparse(full_url)
+        method = request.method
+
+        health_check_id = re.search(
+            r"healthcheck/(?P<health_check_id>[^/]+)/status$", parsed_url.path
+        ).group(  # type: ignore[union-attr]
+            "health_check_id"
+        )
+
+        if method == "GET":
+            self.backend.get_health_check(health_check_id)
+            template = Template(GET_HEALTH_CHECK_STATUS_RESPONSE)
+            return (
+                200,
+                headers,
+                template.render(
+                    timestamp=iso_8601_datetime_with_milliseconds(
+                        datetime.datetime.utcnow()
+                    ),
+                ),
+            )
+
     def not_implemented_response(
         self, request: Any, full_url: str, headers: Any
     ) -> TYPE_RESPONSE:
@@ -846,6 +874,21 @@ GET_HEALTH_CHECK_RESPONSE = """<?xml version="1.0"?>
 </GetHealthCheckResponse>
 """
 
+GET_HEALTH_CHECK_STATUS_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
+<GetHealthCheckStatusResponse>
+   <HealthCheckObservations>
+      <HealthCheckObservation>
+         <IPAddress>127.0.13.37</IPAddress>
+         <Region>us-east-1</Region>
+         <StatusReport>
+            <CheckedTime>{{ timestamp }}</CheckedTime>
+            <Status>Success: HTTP Status Code: 200. Resolved IP: 127.0.13.37. OK</Status>
+         </StatusReport>
+      </HealthCheckObservation>
+   </HealthCheckObservations>
+</GetHealthCheckStatusResponse>
+"""
+
 UPDATE_HOSTED_ZONE_COMMENT_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
 <UpdateHostedZoneCommentResponse>
    <HostedZone>
diff --git a/moto/route53/urls.py b/moto/route53/urls.py
index 82a11174b..ba33771f7 100644
--- a/moto/route53/urls.py
+++ b/moto/route53/urls.py
@@ -32,6 +32,7 @@ url_paths = {
     r"{0}/(?P<api_version>[\d_-]+)/hostedzonecount": Route53().get_hosted_zone_count_response,
     r"{0}/(?P<api_version>[\d_-]+)/healthcheck$": Route53().health_check_response1,
     r"{0}/(?P<api_version>[\d_-]+)/healthcheck/(?P<health_check_id>[^/]+)$": Route53().health_check_response2,
+    r"{0}/(?P<api_version>[\d_-]+)/healthcheck/(?P<health_check_id>[^/]+)/status$": Route53().health_check_status_response,
     r"{0}/(?P<api_version>[\d_-]+)/tags/healthcheck/(?P<zone_id>[^/]+)$": tag_response1,
     r"{0}/(?P<api_version>[\d_-]+)/tags/hostedzone/(?P<zone_id>[^/]+)$": tag_response2,
     r"{0}/(?P<api_version>[\d_-]+)/trafficpolicyinstances/*": Route53().not_implemented_response,
diff --git a/moto/secretsmanager/models.py b/moto/secretsmanager/models.py
index 2d5c572d0..a011f8b59 100644
--- a/moto/secretsmanager/models.py
+++ b/moto/secretsmanager/models.py
@@ -723,7 +723,7 @@ class SecretsManagerBackend(BaseBackend):
         force_delete_without_recovery: bool,
     ) -> Tuple[str, str, float]:
 
-        if recovery_window_in_days and (
+        if recovery_window_in_days is not None and (
             recovery_window_in_days < 7 or recovery_window_in_days > 30
         ):
             raise InvalidParameterException(


Test patch:
diff --git a/other_langs/tests_java/pom.xml b/other_langs/tests_java/pom.xml
index c97055aa9..34c24b111 100644
--- a/other_langs/tests_java/pom.xml
+++ b/other_langs/tests_java/pom.xml
@@ -22,7 +22,7 @@
     <dependency>
       <groupId>software.amazon.awssdk</groupId>
       <artifactId>bom</artifactId>
-      <version>2.20.77</version>
+      <version>2.20.97</version>
       <type>pom</type>
       <scope>import</scope>
     </dependency>
@@ -70,7 +70,7 @@
         <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
         <plugin>
           <artifactId>maven-clean-plugin</artifactId>
-          <version>3.2.0</version>
+          <version>3.3.1</version>
         </plugin>
         <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
         <plugin>
@@ -83,7 +83,7 @@
         </plugin>
         <plugin>
           <artifactId>maven-surefire-plugin</artifactId>
-          <version>3.1.0</version>
+          <version>3.1.2</version>
         </plugin>
         <plugin>
           <artifactId>maven-jar-plugin</artifactId>
@@ -104,7 +104,7 @@
         </plugin>
         <plugin>
           <artifactId>maven-project-info-reports-plugin</artifactId>
-          <version>3.4.4</version>
+          <version>3.4.5</version>
         </plugin>
       </plugins>
     </pluginManagement>
diff --git a/tests/test_cloudfront/test_cloudfront_distributions.py b/tests/test_cloudfront/test_cloudfront_distributions.py
index c35376900..962839823 100644
--- a/tests/test_cloudfront/test_cloudfront_distributions.py
+++ b/tests/test_cloudfront/test_cloudfront_distributions.py
@@ -287,12 +287,15 @@ def test_create_distribution_with_origins():
 @pytest.mark.parametrize("qs", [True, False])
 @pytest.mark.parametrize("smooth", [True, False])
 @pytest.mark.parametrize("ipv6", [True, False])
-def test_create_distribution_with_additional_fields(compress, qs, smooth, ipv6):
+@pytest.mark.parametrize("aliases", [["alias1", "alias2"], ["alias1"]])
+def test_create_distribution_with_additional_fields(
+    compress, qs, smooth, ipv6, aliases
+):
     client = boto3.client("cloudfront", region_name="us-west-1")
 
     config = scaffold.example_distribution_config("ref")
     config["IsIPV6Enabled"] = ipv6
-    config["Aliases"] = {"Quantity": 2, "Items": ["alias1", "alias2"]}
+    config["Aliases"] = {"Quantity": 2, "Items": aliases}
     config["DefaultCacheBehavior"]["ForwardedValues"]["Cookies"] = {
         "Forward": "whitelist",
         "WhitelistedNames": {"Quantity": 1, "Items": ["x-amz-header"]},
@@ -306,9 +309,7 @@ def test_create_distribution_with_additional_fields(compress, qs, smooth, ipv6):
     distribution = resp["Distribution"]
     distribution.should.have.key("DistributionConfig")
     config = distribution["DistributionConfig"]
-    config.should.have.key("Aliases").equals(
-        {"Items": ["alias1", "alias2"], "Quantity": 2}
-    )
+    assert config["Aliases"] == {"Items": aliases, "Quantity": len(aliases)}
 
     config.should.have.key("PriceClass").equals("PriceClass_100")
     config.should.have.key("IsIPV6Enabled").equals(ipv6)
diff --git a/tests/test_route53/test_route53_healthchecks.py b/tests/test_route53/test_route53_healthchecks.py
index c6e3f6cde..3b8210dee 100644
--- a/tests/test_route53/test_route53_healthchecks.py
+++ b/tests/test_route53/test_route53_healthchecks.py
@@ -296,3 +296,36 @@ def test_update_health_check():
     config.should.have.key("Disabled").equals(False)
     config.should.have.key("ChildHealthChecks").equals(["child"])
     config.should.have.key("Regions").equals(["us-east-1", "us-east-2", "us-west-1"])
+
+
+@mock_route53
+def test_health_check_status():
+    client = boto3.client("route53", region_name="us-east-1")
+
+    hc_id = client.create_health_check(
+        CallerReference="callref",
+        HealthCheckConfig={
+            "Type": "CALCULATED",
+            "Inverted": False,
+            "Disabled": False,
+            "HealthThreshold": 1,
+        },
+    )["HealthCheck"]["Id"]
+
+    resp = client.get_health_check_status(HealthCheckId=hc_id)
+    resp["HealthCheckObservations"].should.have.length_of(1)
+
+    observation = resp["HealthCheckObservations"][0]
+    observation.should.have.key("Region").being.equals("us-east-1")
+    observation.should.have.key("IPAddress").being.equals("127.0.13.37")
+    observation.should.have.key("StatusReport")
+    observation["StatusReport"].should.have.key("Status").being.equals(
+        "Success: HTTP Status Code: 200. Resolved IP: 127.0.13.37. OK"
+    )
+
+    with pytest.raises(ClientError) as exc:
+        client.get_health_check_status(HealthCheckId="bad-id")
+
+    err = exc.value.response["Error"]
+    err["Code"].should.equal("NoSuchHealthCheck")
+    err["Message"].should.equal("A health check with id bad-id does not exist.")
diff --git a/tests/test_secretsmanager/test_secretsmanager.py b/tests/test_secretsmanager/test_secretsmanager.py
index 66a921dc2..146052e19 100644
--- a/tests/test_secretsmanager/test_secretsmanager.py
+++ b/tests/test_secretsmanager/test_secretsmanager.py
@@ -355,34 +355,38 @@ def test_delete_secret_fails_with_both_force_delete_flag_and_recovery_window_fla
 
 
 @mock_secretsmanager
-def test_delete_secret_recovery_window_too_short():
+def test_delete_secret_recovery_window_invalid_values():
     conn = boto3.client("secretsmanager", region_name="us-west-2")
 
     conn.create_secret(Name="test-secret", SecretString="foosecret")
 
-    with pytest.raises(ClientError):
-        conn.delete_secret(SecretId="test-secret", RecoveryWindowInDays=6)
-
-
-@mock_secretsmanager
-def test_delete_secret_recovery_window_too_long():
-    conn = boto3.client("secretsmanager", region_name="us-west-2")
-
-    conn.create_secret(Name="test-secret", SecretString="foosecret")
-
-    with pytest.raises(ClientError):
-        conn.delete_secret(SecretId="test-secret", RecoveryWindowInDays=31)
+    for nr in [0, 2, 6, 31, 100]:
+        with pytest.raises(ClientError) as exc:
+            conn.delete_secret(SecretId="test-secret", RecoveryWindowInDays=nr)
+        err = exc.value.response["Error"]
+        assert err["Code"] == "InvalidParameterException"
+        assert (
+            "RecoveryWindowInDays value must be between 7 and 30 days (inclusive)"
+            in err["Message"]
+        )
 
 
 @mock_secretsmanager
 def test_delete_secret_force_no_such_secret_with_invalid_recovery_window():
     conn = boto3.client("secretsmanager", region_name="us-west-2")
 
-    with pytest.raises(ClientError):
-        conn.delete_secret(
-            SecretId=DEFAULT_SECRET_NAME,
-            ForceDeleteWithoutRecovery=True,
-            RecoveryWindowInDays=4,
+    for nr in [0, 2, 6, 31, 100]:
+        with pytest.raises(ClientError) as exc:
+            conn.delete_secret(
+                SecretId="test-secret",
+                RecoveryWindowInDays=nr,
+                ForceDeleteWithoutRecovery=True,
+            )
+        err = exc.value.response["Error"]
+        assert err["Code"] == "InvalidParameterException"
+        assert (
+            "RecoveryWindowInDays value must be between 7 and 30 days (inclusive)"
+            in err["Message"]
         )