Repository: getmoto/moto
Pull request #7693 (2024-05-13 19:35:11)

ResilienceHub: list_app_assessments() can now return pre-configured r…
…esults

Related to #7438 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7693?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.33%. Comparing base [(`b90a540`)](https://app.codecov.io/gh/getmoto/moto/commit/b90a540cf6e7fdcc115420c4c58249512c522ce9?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`46d84a4`)](https://app.codecov.io/gh/getmoto/moto/pull/7693?dropdown=coverage&src=pr&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    #7693   +/-   ##
=======================================
  Coverage   94.32%   94.33%           
=======================================
  Files        1087     1087           
  Lines       92650    92672   +22     
=======================================
+ Hits        87396    87418   +22     
  Misses       5254     5254           
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7693/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/7693/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `29.41% <8.33%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7693/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.30% <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/7693?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/docs/docs/services/resiliencehub.rst b/docs/docs/services/resiliencehub.rst
index fc1394c2f..9b4b16da6 100644
--- a/docs/docs/services/resiliencehub.rst
+++ b/docs/docs/services/resiliencehub.rst
@@ -50,6 +50,42 @@ resiliencehub
 - [ ] list_app_assessment_compliance_drifts
 - [ ] list_app_assessment_resource_drifts
 - [X] list_app_assessments
+  
+        Moto will not actually execute any assessments, so this operation will return an empty list by default.
+        You can use a dedicated API to override this, by configuring a queue of expected results.
+
+        A request to `list_app_assessments` will take the first result from that queue, with subsequent calls with the same parameters returning the same result.
+
+        Calling `list_app_assessments` with a different set of parameters will return the second result from that queue - and so on, or an empty list of the queue is empty.
+
+        Configure this queue by making an HTTP request to `/moto-api/static/resilience-hub-assessments/response`. An example invocation looks like this:
+
+        .. sourcecode:: python
+
+            summary1 = {"appArn": "app_arn1", "appVersion": "some version", ...}
+            summary2 = {"appArn": "app_arn2", ...}
+            results = {"results": [[summary1, summary2], [summary2]], "region": "us-east-1"}
+            resp = requests.post(
+                "http://motoapi.amazonaws.com/moto-api/static/resilience-hub-assessments/response",
+                json=results,
+            )
+
+            assert resp.status_code == 201
+
+            client = boto3.client("lambda", region_name="us-east-1")
+            # First result
+            resp = client.list_app_assessments() # [summary1, summary2]
+            # Second result
+            resp = client.list_app_assessments(assessmentStatus="Pending") # [summary2]
+
+        If you're using MotoServer, make sure to make this request to where MotoServer is running:
+
+        .. sourcecode:: python
+
+            http://localhost:5000/moto-api/static/resilience-hub-assessments/response
+
+        
+
 - [ ] list_app_component_compliances
 - [ ] list_app_component_recommendations
 - [ ] list_app_input_sources
diff --git a/moto/moto_api/_internal/models.py b/moto/moto_api/_internal/models.py
index 1b00d3d5f..70dc10f2a 100644
--- a/moto/moto_api/_internal/models.py
+++ b/moto/moto_api/_internal/models.py
@@ -62,6 +62,14 @@ class MotoAPIBackend(BaseBackend):
         backend = lambda_simple_backends[account_id][region]
         backend.lambda_simple_results_queue.append(result)
 
+    def set_resilience_result(
+        self, result: List[Dict[str, Any]], account_id: str, region: str
+    ) -> None:
+        from moto.resiliencehub.models import resiliencehub_backends
+
+        backend = resiliencehub_backends[account_id][region]
+        backend.app_assessments_queue.append(result)
+
     def set_sagemaker_result(
         self,
         body: str,
diff --git a/moto/moto_api/_internal/responses.py b/moto/moto_api/_internal/responses.py
index 1aceb29e4..f89899b44 100644
--- a/moto/moto_api/_internal/responses.py
+++ b/moto/moto_api/_internal/responses.py
@@ -197,6 +197,24 @@ class MotoAPIResponse(BaseResponse):
             )
         return 201, {}, ""
 
+    def set_resilience_result(
+        self,
+        request: Any,
+        full_url: str,  # pylint: disable=unused-argument
+        headers: Any,
+    ) -> TYPE_RESPONSE:
+        from .models import moto_api_backend
+
+        body = self._get_body(headers, request)
+        account_id = body.get("account_id", DEFAULT_ACCOUNT_ID)
+        region = body.get("region", "us-east-1")
+
+        for result in body.get("results", []):
+            moto_api_backend.set_resilience_result(
+                result=result, account_id=account_id, region=region
+            )
+        return 201, {}, ""
+
     def set_sagemaker_result(
         self,
         request: Any,
diff --git a/moto/moto_api/_internal/urls.py b/moto/moto_api/_internal/urls.py
index 278ed7249..2195f2ee5 100644
--- a/moto/moto_api/_internal/urls.py
+++ b/moto/moto_api/_internal/urls.py
@@ -18,6 +18,7 @@ url_paths = {
     "{0}/moto-api/static/ce/cost-and-usage-results": response_instance.set_ce_cost_usage_result,
     "{0}/moto-api/static/inspector2/findings-results": response_instance.set_inspector2_findings_result,
     "{0}/moto-api/static/lambda-simple/response": response_instance.set_lambda_simple_result,
+    "{0}/moto-api/static/resilience-hub-assessments/response": response_instance.set_resilience_result,
     "{0}/moto-api/static/sagemaker/endpoint-results": response_instance.set_sagemaker_result,
     "{0}/moto-api/static/rds-data/statement-results": response_instance.set_rds_data_result,
     "{0}/moto-api/state-manager/get-transition": response_instance.get_transition,
diff --git a/moto/resiliencehub/models.py b/moto/resiliencehub/models.py
index 8a5cf568d..d6ec83ab8 100644
--- a/moto/resiliencehub/models.py
+++ b/moto/resiliencehub/models.py
@@ -186,6 +186,9 @@ class ResilienceHubBackend(BaseBackend):
         self.policies: Dict[str, Policy] = dict()
         self.tagger = TaggingService()
 
+        self.app_assessments_queue: List[List[Dict[str, Any]]] = []
+        self.app_assessments_results: Dict[str, List[Dict[str, Any]]] = {}
+
     def create_app(
         self,
         assessment_schedule: str,
@@ -251,7 +254,49 @@ class ResilienceHubBackend(BaseBackend):
             app_summaries.reverse()
         return app_summaries
 
-    def list_app_assessments(self) -> List[str]:
+    def list_app_assessments(self, request_identifier: str) -> List[Dict[str, Any]]:
+        """
+        Moto will not actually execute any assessments, so this operation will return an empty list by default.
+        You can use a dedicated API to override this, by configuring a queue of expected results.
+
+        A request to `list_app_assessments` will take the first result from that queue, with subsequent calls with the same parameters returning the same result.
+
+        Calling `list_app_assessments` with a different set of parameters will return the second result from that queue - and so on, or an empty list of the queue is empty.
+
+        Configure this queue by making an HTTP request to `/moto-api/static/resilience-hub-assessments/response`. An example invocation looks like this:
+
+        .. sourcecode:: python
+
+            summary1 = {"appArn": "app_arn1", "appVersion": "some version", ...}
+            summary2 = {"appArn": "app_arn2", ...}
+            results = {"results": [[summary1, summary2], [summary2]], "region": "us-east-1"}
+            resp = requests.post(
+                "http://motoapi.amazonaws.com/moto-api/static/resilience-hub-assessments/response",
+                json=results,
+            )
+
+            assert resp.status_code == 201
+
+            client = boto3.client("lambda", region_name="us-east-1")
+            # First result
+            resp = client.list_app_assessments() # [summary1, summary2]
+            # Second result
+            resp = client.list_app_assessments(assessmentStatus="Pending") # [summary2]
+
+        If you're using MotoServer, make sure to make this request to where MotoServer is running:
+
+        .. sourcecode:: python
+
+            http://localhost:5000/moto-api/static/resilience-hub-assessments/response
+
+        """
+        if request_identifier in self.app_assessments_results:
+            return self.app_assessments_results[request_identifier]
+        if self.app_assessments_queue:
+            self.app_assessments_results[request_identifier] = (
+                self.app_assessments_queue.pop(0)
+            )
+            return self.app_assessments_results[request_identifier]
         return []
 
     def describe_app(self, app_arn: str) -> App:
diff --git a/moto/resiliencehub/responses.py b/moto/resiliencehub/responses.py
index 7a8f3b903..651e9451c 100644
--- a/moto/resiliencehub/responses.py
+++ b/moto/resiliencehub/responses.py
@@ -97,7 +97,23 @@ class ResilienceHubResponse(BaseResponse):
         )
 
     def list_app_assessments(self) -> str:
-        summaries = self.resiliencehub_backend.list_app_assessments()
+        supported_params = [
+            "appArn",
+            "assessmentName",
+            "assessmentStatus",
+            "complianceStatus",
+            "invoker",
+            "maxResults",
+            "nextToken",
+            "reverseOrder",
+        ]
+        provided_params = [p for p in self.querystring.keys() if p in supported_params]
+        request_identifier = json.dumps(
+            {key: self.querystring[key] for key in sorted(provided_params)}
+        )
+        summaries = self.resiliencehub_backend.list_app_assessments(
+            request_identifier=request_identifier,
+        )
         return json.dumps(dict(assessmentSummaries=summaries))
 
     def describe_app(self) -> str:


Test patch:
diff --git a/tests/test_resiliencehub/test_resiliencehub.py b/tests/test_resiliencehub/test_resiliencehub.py
index 5e470757c..fcf936261 100644
--- a/tests/test_resiliencehub/test_resiliencehub.py
+++ b/tests/test_resiliencehub/test_resiliencehub.py
@@ -2,9 +2,10 @@ from uuid import uuid4
 
 import boto3
 import pytest
+import requests
 from botocore.exceptions import ClientError
 
-from moto import mock_aws
+from moto import mock_aws, settings
 
 # 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
@@ -209,6 +210,44 @@ def test_list_apps():
 def test_list_app_assessments():
     client = boto3.client("resiliencehub", region_name="ap-southeast-1")
     assert client.list_app_assessments()["assessmentSummaries"] == []
+    assert client.list_app_assessments(appArn="asdf")["assessmentSummaries"] == []
+
+    # Setup
+    base_url = (
+        settings.test_server_mode_endpoint()
+        if settings.TEST_SERVER_MODE
+        else "http://motoapi.amazonaws.com"
+    )
+    # Configure results for the first two requests
+    summary1 = {"appArn": "app_arn1", "appVersion": "some version"}
+    summary2 = {"appArn": "app_arn2", "assessmentName": "assessment 2"}
+    results = {
+        "results": [[summary1, summary2], [summary2]],
+        "region": "ap-southeast-1",
+    }
+    resp = requests.post(
+        f"{base_url}/moto-api/static/resilience-hub-assessments/response",
+        json=results,
+    )
+    assert resp.status_code == 201
+
+    # First response
+    assert client.list_app_assessments()["assessmentSummaries"] == [summary1, summary2]
+
+    # Stays the same on subsequent invocations
+    assert client.list_app_assessments()["assessmentSummaries"] == [summary1, summary2]
+
+    # Second response with different args
+    assert client.list_app_assessments(appArn="asdf")["assessmentSummaries"] == [
+        summary2
+    ]
+
+    # Third response returns an empty response, because nothing else is configured
+    assert client.list_app_assessments(appArn="diff")["assessmentSummaries"] == []
+
+    # First response in different region also returns nothing, because we only configured this in ap-southeast
+    us_client = boto3.client("resiliencehub", region_name="us-east-1")
+    assert us_client.list_app_assessments()["assessmentSummaries"] == []
 
 
 @mock_aws