Repository: getmoto/moto
Pull request #7480 (2024-03-16 23:24:57)

ResilienceHub - Resource-related functionality
- create_app_version_app_component()
- create_app_version_resource()
- import_resources_to_draft_app_version()
- list_app_version_app_components()
- list_app_version_resources()
- list_app_versions()
- publish_app_version()

See #7438 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7480?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 `98.38710%` with `2 lines` in your changes are missing coverage. Please review.
> Project coverage is 94.05%. Comparing base [(`9aef694`)](https://app.codecov.io/gh/getmoto/moto/commit/9aef6944da6a6f1b84be99cefb34f1724e6ca58f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`8f73670`)](https://app.codecov.io/gh/getmoto/moto/pull/7480?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 108 commits behind head on master.

| [Files](https://app.codecov.io/gh/getmoto/moto/pull/7480?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/resiliencehub/exceptions.py](https://app.codecov.io/gh/getmoto/moto/pull/7480?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9yZXNpbGllbmNlaHViL2V4Y2VwdGlvbnMucHk=) | 66.66% | [1 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/7480?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) |
| [moto/resiliencehub/models.py](https://app.codecov.io/gh/getmoto/moto/pull/7480?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#diff-bW90by9yZXNpbGllbmNlaHViL21vZGVscy5weQ==) | 98.76% | [1 Missing :warning: ](https://app.codecov.io/gh/getmoto/moto/pull/7480?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    #7480      +/-   ##
==========================================
- Coverage   95.88%   94.05%   -1.83%     
==========================================
  Files         843     1064     +221     
  Lines       82578    91379    +8801     
==========================================
+ Hits        79178    85946    +6768     
- Misses       3400     5433    +2033     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7480/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/7480/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `29.64% <0.00%> (-2.89%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7480/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.02% <98.38%> (-1.83%)` | :arrow_down: |

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/7480?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 fedeb40d1..a00754251 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -6218,13 +6218,13 @@
 
 ## resiliencehub
 <details>
-<summary>18% implemented</summary>
+<summary>31% implemented</summary>
 
 - [ ] add_draft_app_version_resource_mappings
 - [ ] batch_update_recommendation_status
 - [X] create_app
-- [ ] create_app_version_app_component
-- [ ] create_app_version_resource
+- [X] create_app_version_app_component
+- [X] create_app_version_resource
 - [ ] create_recommendation_template
 - [X] create_resiliency_policy
 - [ ] delete_app
@@ -6243,17 +6243,17 @@
 - [ ] describe_app_version_template
 - [ ] describe_draft_app_version_resources_import_status
 - [X] describe_resiliency_policy
-- [ ] import_resources_to_draft_app_version
+- [X] import_resources_to_draft_app_version
 - [ ] list_alarm_recommendations
 - [ ] list_app_assessment_compliance_drifts
 - [X] list_app_assessments
 - [ ] list_app_component_compliances
 - [ ] list_app_component_recommendations
 - [ ] list_app_input_sources
-- [ ] list_app_version_app_components
+- [X] list_app_version_app_components
 - [ ] list_app_version_resource_mappings
-- [ ] list_app_version_resources
-- [ ] list_app_versions
+- [X] list_app_version_resources
+- [X] list_app_versions
 - [X] list_apps
 - [ ] list_recommendation_templates
 - [X] list_resiliency_policies
@@ -6262,7 +6262,7 @@
 - [X] list_tags_for_resource
 - [ ] list_test_recommendations
 - [ ] list_unsupported_app_version_resources
-- [ ] publish_app_version
+- [X] publish_app_version
 - [ ] put_draft_app_version_template
 - [ ] remove_draft_app_version_resource_mappings
 - [ ] resolve_app_version_resources
diff --git a/docs/docs/services/resiliencehub.rst b/docs/docs/services/resiliencehub.rst
index b326081f7..1a0eddf09 100644
--- a/docs/docs/services/resiliencehub.rst
+++ b/docs/docs/services/resiliencehub.rst
@@ -21,8 +21,8 @@ resiliencehub
         The ClientToken-parameter is not yet implemented
         
 
-- [ ] create_app_version_app_component
-- [ ] create_app_version_resource
+- [X] create_app_version_app_component
+- [X] create_app_version_resource
 - [ ] create_recommendation_template
 - [X] create_resiliency_policy
   
@@ -45,17 +45,17 @@ resiliencehub
 - [ ] describe_app_version_template
 - [ ] describe_draft_app_version_resources_import_status
 - [X] describe_resiliency_policy
-- [ ] import_resources_to_draft_app_version
+- [X] import_resources_to_draft_app_version
 - [ ] list_alarm_recommendations
 - [ ] list_app_assessment_compliance_drifts
 - [X] list_app_assessments
 - [ ] list_app_component_compliances
 - [ ] list_app_component_recommendations
 - [ ] list_app_input_sources
-- [ ] list_app_version_app_components
+- [X] list_app_version_app_components
 - [ ] list_app_version_resource_mappings
-- [ ] list_app_version_resources
-- [ ] list_app_versions
+- [X] list_app_version_resources
+- [X] list_app_versions
 - [X] list_apps
   
         The FromAssessmentTime/ToAssessmentTime-parameters are not yet implemented
@@ -68,7 +68,7 @@ resiliencehub
 - [X] list_tags_for_resource
 - [ ] list_test_recommendations
 - [ ] list_unsupported_app_version_resources
-- [ ] publish_app_version
+- [X] publish_app_version
 - [ ] put_draft_app_version_template
 - [ ] remove_draft_app_version_resource_mappings
 - [ ] resolve_app_version_resources
diff --git a/moto/resiliencehub/exceptions.py b/moto/resiliencehub/exceptions.py
index 88c400049..8eac01a43 100644
--- a/moto/resiliencehub/exceptions.py
+++ b/moto/resiliencehub/exceptions.py
@@ -12,6 +12,11 @@ class AppNotFound(ResourceNotFound):
         super().__init__(f"App not found for appArn {arn}")
 
 
+class AppVersionNotFound(ResourceNotFound):
+    def __init__(self) -> None:
+        super().__init__("App Version not found")
+
+
 class ResiliencyPolicyNotFound(ResourceNotFound):
     def __init__(self, arn: str):
         super().__init__(f"ResiliencyPolicy {arn} not found")
diff --git a/moto/resiliencehub/models.py b/moto/resiliencehub/models.py
index 173ed2437..8a5cf568d 100644
--- a/moto/resiliencehub/models.py
+++ b/moto/resiliencehub/models.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Optional
 
 from moto.core.base_backend import BackendDict, BaseBackend
 from moto.core.common_models import BaseModel
@@ -7,7 +7,7 @@ from moto.moto_api._internal import mock_random
 from moto.utilities.paginator import paginate
 from moto.utilities.tagging_service import TaggingService
 
-from .exceptions import AppNotFound, ResiliencyPolicyNotFound
+from .exceptions import AppNotFound, AppVersionNotFound, ResiliencyPolicyNotFound
 
 PAGINATION_MODEL = {
     "list_apps": {
@@ -25,6 +25,20 @@ PAGINATION_MODEL = {
 }
 
 
+class AppComponent(BaseModel):
+    def __init__(self, _id: str, name: str, _type: str):
+        self.id = _id
+        self.name = name
+        self.type = _type
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "id": self.id,
+            "name": self.name,
+            "type": self.type,
+        }
+
+
 class App(BaseModel):
     def __init__(
         self,
@@ -48,6 +62,16 @@ class App(BaseModel):
         self.policy_arn = policy_arn
         self.resilience_score = 0.0
         self.status = "Active"
+        self.app_versions: List[AppVersion] = []
+
+        app_version = AppVersion(app_arn=self.arn, version_name=None, identifier=0)
+        self.app_versions.append(app_version)
+
+    def get_version(self, version_name: str) -> "AppVersion":
+        for v in self.app_versions:
+            if v.app_version == version_name:
+                return v
+        raise AppVersionNotFound
 
     def to_json(self) -> Dict[str, Any]:
         resp = {
@@ -55,6 +79,7 @@ class App(BaseModel):
             "assessmentSchedule": self.assessment_schedule,
             "complianceStatus": self.compliance_status,
             "creationTime": self.creation_time,
+            "driftStatus": "NotChecked",
             "name": self.name,
             "resilienceScore": self.resilience_score,
             "status": self.status,
@@ -71,6 +96,54 @@ class App(BaseModel):
         return resp
 
 
+class Resource:
+    def __init__(
+        self,
+        logical_resource_id: Dict[str, Any],
+        physical_resource_id: str,
+        resource_type: str,
+        components: List[AppComponent],
+    ):
+        self.logical_resource_id = logical_resource_id
+        self.physical_resource_id = physical_resource_id
+        self.resource_type = resource_type
+        self.components = components
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "appComponents": [c.to_json() for c in self.components],
+            "resourceType": self.resource_type,
+            "logicalResourceId": self.logical_resource_id,
+            "physicalResourceId": {"identifier": self.physical_resource_id},
+            "resourceName": self.logical_resource_id["identifier"],
+        }
+
+
+class AppVersion(BaseModel):
+    def __init__(self, app_arn: str, version_name: Optional[str], identifier: int):
+        self.app_arn = app_arn
+        self.eks_sources: List[Dict[str, Any]] = []
+        self.source_arns: List[str] = []
+        self.terraform_sources: List[Dict[str, str]] = []
+        self.app_version = "release" if version_name else "draft"
+        self.identifier = identifier
+        self.creation_time = unix_time()
+        self.version_name = version_name
+        self.app_components: List[AppComponent] = []
+        self.status = "Pending"
+        self.resources: List[Resource] = []
+
+    def to_json(self) -> Dict[str, Any]:
+        resp = {
+            "appVersion": self.app_version,
+            "creationTime": self.creation_time,
+            "identifier": self.identifier,
+        }
+        if self.version_name:
+            resp["versionName"] = self.version_name
+        return resp
+
+
 class Policy(BaseModel):
     def __init__(
         self,
@@ -208,5 +281,88 @@ class ResilienceHubBackend(BaseBackend):
     def list_tags_for_resource(self, resource_arn: str) -> Dict[str, str]:
         return self.tagger.get_tag_dict_for_resource(resource_arn)
 
+    def import_resources_to_draft_app_version(
+        self,
+        app_arn: str,
+        eks_sources: List[Dict[str, Any]],
+        source_arns: List[str],
+        terraform_sources: List[Dict[str, str]],
+    ) -> AppVersion:
+        app = self.describe_app(app_arn)
+        app_version = app.get_version("draft")
+
+        app_version.eks_sources.extend(eks_sources)
+        app_version.source_arns.extend(source_arns)
+        app_version.terraform_sources.extend(terraform_sources)
+
+        # Default AppComponent when importing data
+        # AWS seems to create other components as well, based on the provided sources
+        app_version.app_components.append(
+            AppComponent(
+                _id="appcommon",
+                name="appcommon",
+                _type="AWS::ResilienceHub::AppCommonAppComponent",
+            )
+        )
+        return app_version
+
+    def create_app_version_app_component(
+        self, app_arn: str, name: str, _type: str
+    ) -> AppComponent:
+        app = self.describe_app(app_arn)
+        app_version = app.get_version("draft")
+        component = AppComponent(_id=name, name=name, _type=_type)
+        app_version.app_components.append(component)
+        return component
+
+    def list_app_version_app_components(
+        self, app_arn: str, app_version: str
+    ) -> List[AppComponent]:
+        app = self.describe_app(app_arn)
+        return app.get_version(app_version).app_components
+
+    def create_app_version_resource(
+        self,
+        app_arn: str,
+        app_components: List[str],
+        logical_resource_id: Dict[str, str],
+        physical_resource_id: str,
+        resource_type: str,
+    ) -> Resource:
+        app = self.describe_app(app_arn)
+        app_version = app.get_version("draft")
+
+        components = [c for c in app_version.app_components if c.id in app_components]
+
+        resource = Resource(
+            logical_resource_id=logical_resource_id,
+            physical_resource_id=physical_resource_id,
+            resource_type=resource_type,
+            components=components,
+        )
+        app_version.resources.append(resource)
+        return resource
+
+    def list_app_version_resources(
+        self, app_arn: str, app_version: str
+    ) -> List[Resource]:
+        app = self.describe_app(app_arn)
+        return app.get_version(app_version).resources
+
+    def list_app_versions(self, app_arn: str) -> List[AppVersion]:
+        app = self.describe_app(app_arn)
+        return app.app_versions
+
+    def publish_app_version(self, app_arn: str, version_name: str) -> AppVersion:
+        app = self.describe_app(app_arn)
+        version = AppVersion(
+            app_arn=app_arn, version_name=version_name, identifier=len(app.app_versions)
+        )
+        for old_version in app.app_versions:
+            if old_version.app_version == "release":
+                old_version.app_version = str(old_version.identifier)
+        app.app_versions.append(version)
+        return version
+
 
 resiliencehub_backends = BackendDict(ResilienceHubBackend, "resiliencehub")
diff --git a/moto/resiliencehub/responses.py b/moto/resiliencehub/responses.py
index c9b530613..7a8f3b903 100644
--- a/moto/resiliencehub/responses.py
+++ b/moto/resiliencehub/responses.py
@@ -157,3 +157,99 @@ class ResilienceHubResponse(BaseResponse):
             resource_arn=resource_arn,
         )
         return json.dumps(dict(tags=tags))
+
+    def import_resources_to_draft_app_version(self) -> str:
+        app_arn = self._get_param("appArn")
+        eks_sources = self._get_param("eksSources")
+        source_arns = self._get_param("sourceArns")
+        terraform_sources = self._get_param("terraformSources")
+
+        app_version = self.resiliencehub_backend.import_resources_to_draft_app_version(
+            app_arn=app_arn,
+            eks_sources=eks_sources,
+            source_arns=source_arns,
+            terraform_sources=terraform_sources,
+        )
+        return json.dumps(
+            {
+                "appArn": app_version.app_arn,
+                "appVersion": app_version.version_name,
+                "eksSources": eks_sources,
+                "sourceArns": source_arns,
+                "status": app_version.status,
+                "terraformSources": terraform_sources,
+            }
+        )
+
+    def list_app_version_app_components(self) -> str:
+        app_arn = self._get_param("appArn")
+        app_version = self._get_param("appVersion")
+        components = self.resiliencehub_backend.list_app_version_app_components(
+            app_arn, app_version
+        )
+        return json.dumps(
+            {
+                "appArn": app_arn,
+                "appVersion": app_version,
+                "appComponents": [c.to_json() for c in components],
+            }
+        )
+
+    def create_app_version_app_component(self) -> str:
+        app_arn = self._get_param("appArn")
+        name = self._get_param("name")
+        _type = self._get_param("type")
+        component = self.resiliencehub_backend.create_app_version_app_component(
+            app_arn=app_arn,
+            name=name,
+            _type=_type,
+        )
+        return json.dumps(
+            {
+                "appArn": app_arn,
+                "appComponent": component.to_json(),
+                "appVersion": "draft",
+            }
+        )
+
+    def create_app_version_resource(self) -> str:
+        app_arn = self._get_param("appArn")
+        app_components = self._get_param("appComponents")
+        logical_resource_id = self._get_param("logicalResourceId")
+        physical_resource_id = self._get_param("physicalResourceId")
+        resource_type = self._get_param("resourceType")
+
+        resource = self.resiliencehub_backend.create_app_version_resource(
+            app_arn=app_arn,
+            app_components=app_components,
+            logical_resource_id=logical_resource_id,
+            physical_resource_id=physical_resource_id,
+            resource_type=resource_type,
+        )
+
+        return json.dumps(
+            {
+                "appArn": app_arn,
+                "appVersion": "draft",
+                "physicalResource": resource.to_json(),
+            }
+        )
+
+    def list_app_version_resources(self) -> str:
+        app_arn = self._get_param("appArn")
+        app_version = self._get_param("appVersion")
+        resources = self.resiliencehub_backend.list_app_version_resources(
+            app_arn, app_version
+        )
+        return json.dumps({"physicalResources": [r.to_json() for r in resources]})
+
+    def list_app_versions(self) -> str:
+        app_arn = self._get_param("appArn")
+        versions = self.resiliencehub_backend.list_app_versions(app_arn)
+        return json.dumps({"appVersions": [v.to_json() for v in versions]})
+
+    def publish_app_version(self) -> str:
+        app_arn = self._get_param("appArn")
+        version_name = self._get_param("versionName")
+        version = self.resiliencehub_backend.publish_app_version(app_arn, version_name)
+        return json.dumps({"appArn": app_arn, **version.to_json()})
diff --git a/moto/resiliencehub/urls.py b/moto/resiliencehub/urls.py
index a39663688..09e96ef08 100644
--- a/moto/resiliencehub/urls.py
+++ b/moto/resiliencehub/urls.py
@@ -7,12 +7,19 @@ url_bases = [
 
 url_paths = {
     "{0}/create-app$": ResilienceHubResponse.dispatch,
+    "{0}/create-app-version-app-component$": ResilienceHubResponse.dispatch,
+    "{0}/create-app-version-resource$": ResilienceHubResponse.dispatch,
     "{0}/create-resiliency-policy$": ResilienceHubResponse.dispatch,
     "{0}/describe-app$": ResilienceHubResponse.dispatch,
     "{0}/describe-resiliency-policy$": ResilienceHubResponse.dispatch,
+    "{0}/import-resources-to-draft-app-version$": ResilienceHubResponse.dispatch,
     "{0}/list-apps$": ResilienceHubResponse.dispatch,
     "{0}/list-app-assessments$": ResilienceHubResponse.dispatch,
+    "{0}/list-app-versions$": ResilienceHubResponse.dispatch,
+    "{0}/list-app-version-app-components$": ResilienceHubResponse.dispatch,
+    "{0}/list-app-version-resources$": ResilienceHubResponse.dispatch,
     "{0}/list-resiliency-policies$": ResilienceHubResponse.dispatch,
+    "{0}/publish-app-version$": ResilienceHubResponse.dispatch,
     "{0}/tags/.+$": ResilienceHubResponse.dispatch,
     "{0}/tags/(?P<arn_prefix>[^/]+)/(?P<workspace_id>[^/]+)$": ResilienceHubResponse.method_dispatch(
         ResilienceHubResponse.tags  # type: ignore


Test patch:
diff --git a/tests/test_resiliencehub/test_resiliencyhub_resources.py b/tests/test_resiliencehub/test_resiliencyhub_resources.py
new file mode 100644
index 000000000..b3d05b23d
--- /dev/null
+++ b/tests/test_resiliencehub/test_resiliencyhub_resources.py
@@ -0,0 +1,162 @@
+import boto3
+
+from moto import mock_aws
+
+
+@mock_aws
+def test_import_resources_to_draft_app_version():
+    client = boto3.client("resiliencehub", region_name="us-east-2")
+    app_arn = client.create_app(name="myapp")["app"]["appArn"]
+
+    components = client.list_app_version_app_components(
+        appArn=app_arn, appVersion="draft"
+    )
+    assert components["appArn"] == app_arn
+    assert components["appComponents"] == []
+    assert components["appVersion"] == "draft"
+
+    resp = client.import_resources_to_draft_app_version(
+        appArn=app_arn,
+        eksSources=[{"eksClusterArn": "some-arn", "namespaces": ["eks/ns/1"]}],
+        importStrategy="AddOnly",
+        sourceArns=["sarn1", "sarn2"],
+        terraformSources=[{"s3StateFileUrl": "tf://url"}],
+    )
+    assert resp["appArn"] == app_arn
+    assert resp["eksSources"] == [
+        {"eksClusterArn": "some-arn", "namespaces": ["eks/ns/1"]}
+    ]
+    assert resp["sourceArns"] == ["sarn1", "sarn2"]
+    assert resp["status"] == "Pending"
+    assert resp["terraformSources"] == [{"s3StateFileUrl": "tf://url"}]
+
+    components = client.list_app_version_app_components(
+        appArn=app_arn, appVersion="draft"
+    )["appComponents"]
+    assert {
+        "id": "appcommon",
+        "name": "appcommon",
+        "type": "AWS::ResilienceHub::AppCommonAppComponent",
+    } in components
+
+
+@mock_aws
+def test_create_app_version_app_component():
+    client = boto3.client("resiliencehub", region_name="us-east-2")
+    app_arn = client.create_app(name="myapp")["app"]["appArn"]
+    component = client.create_app_version_app_component(
+        appArn=app_arn,
+        name="my_databases",
+        type="AWS::ResilienceHub::DatabaseAppComponent",
+    )
+    assert component["appArn"] == app_arn
+    assert component["appVersion"] == "draft"
+    assert component["appComponent"]["id"] == "my_databases"
+    assert component["appComponent"]["name"] == "my_databases"
+    assert (
+        component["appComponent"]["type"] == "AWS::ResilienceHub::DatabaseAppComponent"
+    )
+
+    components = client.list_app_version_app_components(
+        appArn=app_arn, appVersion="draft"
+    )["appComponents"]
+    assert components == [
+        {
+            "id": "my_databases",
+            "name": "my_databases",
+            "type": "AWS::ResilienceHub::DatabaseAppComponent",
+        }
+    ]
+
+
+@mock_aws
+def test_create_app_version_resource():
+    client = boto3.client("resiliencehub", region_name="us-east-2")
+    app_arn = client.create_app(name="myapp")["app"]["appArn"]
+
+    component = client.create_app_version_app_component(
+        appArn=app_arn,
+        name="my_databases",
+        type="AWS::ResilienceHub::DatabaseAppComponent",
+    )["appComponent"]
+
+    resp = client.create_app_version_resource(
+        appArn=app_arn,
+        appComponents=["my_databases"],
+        logicalResourceId={
+            "identifier": "myres",
+        },
+        physicalResourceId="myphys",
+        resourceType="AWS::Lambda::Function",
+    )
+    assert resp["appArn"] == app_arn
+    assert resp["appVersion"] == "draft"
+    assert resp["physicalResource"]["appComponents"] == [component]
+    assert resp["physicalResource"]["logicalResourceId"] == {"identifier": "myres"}
+    assert resp["physicalResource"]["physicalResourceId"]["identifier"] == "myphys"
+    assert resp["physicalResource"]["resourceName"] == "myres"
+    assert resp["physicalResource"]["resourceType"] == "AWS::Lambda::Function"
+
+    resources = client.list_app_version_resources(appArn=app_arn, appVersion="draft")
+    assert resources["physicalResources"] == [resp["physicalResource"]]
+
+
+@mock_aws
+def test_create_app_version_resource_with_unknown_component():
+    client = boto3.client("resiliencehub", region_name="us-east-2")
+    app_arn = client.create_app(name="myapp")["app"]["appArn"]
+
+    # Not sure how AWS behaves, when providing an unknown appComponent
+    # But let's try to be flexible in what we accept
+    resp = client.create_app_version_resource(
+        appArn=app_arn,
+        appComponents=["unknown"],
+        logicalResourceId={
+            "identifier": "myres",
+        },
+        physicalResourceId="myphys",
+        resourceType="AWS::Lambda::Function",
+    )
+    assert resp["physicalResource"]["appComponents"] == []
+
+
+@mock_aws
+def test_publish():
+    client = boto3.client("resiliencehub", region_name="us-east-2")
+    app_arn = client.create_app(name="myapp")["app"]["appArn"]
+
+    versions = client.list_app_versions(appArn=app_arn)["appVersions"]
+    assert len(versions) == 1
+    assert versions[0]["appVersion"] == "draft"
+    assert versions[0]["creationTime"]
+    assert versions[0]["identifier"] == 0
+
+    client.import_resources_to_draft_app_version(
+        appArn=app_arn,
+        eksSources=[{"eksClusterArn": "some-arn", "namespaces": ["eks/ns/1"]}],
+        importStrategy="AddOnly",
+        sourceArns=["sarn1", "sarn2"],
+        terraformSources=[{"s3StateFileUrl": "tf://url"}],
+    )
+
+    publish = client.publish_app_version(appArn=app_arn, versionName="v1")
+    assert publish["appArn"] == app_arn
+    assert publish["appVersion"] == "release"
+    assert publish["identifier"] == 1
+    assert publish["versionName"] == "v1"
+
+    versions = client.list_app_versions(appArn=app_arn)["appVersions"]
+    assert len(versions) == 2
+    assert versions[1]["appVersion"] == "release"
+    assert versions[1]["identifier"] == 1
+    assert versions[1]["versionName"] == "v1"
+
+    client.publish_app_version(appArn=app_arn, versionName="v2")
+
+    versions = client.list_app_versions(appArn=app_arn)["appVersions"]
+    assert len(versions) == 3
+    for v in versions:
+        del v["creationTime"]
+
+    assert {"appVersion": "release", "identifier": 2, "versionName": "v2"} in versions
+    assert {"appVersion": "1", "identifier": 1, "versionName": "v1"} in versions