Repository: getmoto/moto
Pull request #6824 (2023-09-18 00:30:53)

Opensearch: Add the `list_domain_names` method support
Add the Opensearch `list_domain_names` method support. The pagination functionality hasn't been implemented yet, just the base configuration for listing all the domain names for Opensearch.


Patch:
diff --git a/docs/docs/services/opensearch.rst b/docs/docs/services/opensearch.rst
index a6728af5d..c7196e50d 100644
--- a/docs/docs/services/opensearch.rst
+++ b/docs/docs/services/opensearch.rst
@@ -61,7 +61,7 @@ opensearch
 - [ ] get_package_version_history
 - [ ] get_upgrade_history
 - [ ] get_upgrade_status
-- [ ] list_domain_names
+- [x] list_domain_names
 - [ ] list_domains_for_package
 - [ ] list_instance_type_details
 - [ ] list_packages_for_domain
diff --git a/moto/dynamodb/models/dynamo_type.py b/moto/dynamodb/models/dynamo_type.py
index d6554a25c..fe44ab17f 100644
--- a/moto/dynamodb/models/dynamo_type.py
+++ b/moto/dynamodb/models/dynamo_type.py
@@ -196,7 +196,7 @@ class DynamoType(object):
 
     def to_json(self) -> Dict[str, Any]:
         # Returns a regular JSON object where the value can still be/contain a DynamoType
-        if self.is_binary():
+        if self.is_binary() and isinstance(self.value, bytes):
             # Binary data cannot be represented in JSON
             # AWS returns a base64-encoded value - the SDK's then convert that back
             return {self.type: base64.b64encode(self.value).decode("utf-8")}
diff --git a/moto/es/urls.py b/moto/es/urls.py
index 9c8322d6b..b26bfd9ca 100644
--- a/moto/es/urls.py
+++ b/moto/es/urls.py
@@ -10,6 +10,7 @@ url_paths = {
     "{0}/2015-01-01/domain$": ElasticsearchServiceResponse.list_domains,
     "{0}/2015-01-01/es/domain$": ElasticsearchServiceResponse.domains,
     "{0}/2015-01-01/es/domain/(?P<domainname>[^/]+)": ElasticsearchServiceResponse.domain,
+    "{0}/2021-01-01/domain$": OpenSearchServiceResponse.dispatch,
     "{0}/2021-01-01/opensearch/compatibleVersions": OpenSearchServiceResponse.dispatch,
     "{0}/2021-01-01/opensearch/domain": OpenSearchServiceResponse.dispatch,
     "{0}/2021-01-01/opensearch/domain/(?P<domainname>[^/]+)": OpenSearchServiceResponse.dispatch,
diff --git a/moto/opensearch/exceptions.py b/moto/opensearch/exceptions.py
index e0c3a9a33..b5b3f9fe9 100644
--- a/moto/opensearch/exceptions.py
+++ b/moto/opensearch/exceptions.py
@@ -5,3 +5,10 @@ from moto.core.exceptions import JsonRESTError
 class ResourceNotFoundException(JsonRESTError):
     def __init__(self, name: str):
         super().__init__("ResourceNotFoundException", f"Domain not found: {name}")
+
+
+class EngineTypeNotFoundException(JsonRESTError):
+    def __init__(self, domain_name: str):
+        super().__init__(
+            "EngineTypeNotFoundException", f"Engine Type not found: {domain_name}"
+        )
diff --git a/moto/opensearch/models.py b/moto/opensearch/models.py
index 4ef254e49..d2b724021 100644
--- a/moto/opensearch/models.py
+++ b/moto/opensearch/models.py
@@ -2,9 +2,9 @@ from typing import Any, Dict, List, Optional
 
 from moto.core import BaseBackend, BackendDict, BaseModel
 from moto.utilities.tagging_service import TaggingService
-from .data import compatible_versions
-from .exceptions import ResourceNotFoundException
 
+from .data import compatible_versions
+from .exceptions import ResourceNotFoundException, EngineTypeNotFoundException
 
 default_cluster_config = {
     "InstanceType": "t3.small.search",
@@ -327,5 +327,27 @@ class OpenSearchServiceBackend(BaseBackend):
     def remove_tags(self, arn: str, tag_keys: List[str]) -> None:
         self.tagger.untag_resource_using_names(arn, tag_keys)
 
+    def list_domain_names(self, engine_type: str) -> List[Dict[str, str]]:
+        domains = []
+        for domain in self.domains.values():
+            if engine_type:
+                if engine_type in domain.engine_version:
+                    domains.append(
+                        {
+                            "DomainName": domain.domain_name,
+                            "EngineType": engine_type.split("_")[0],
+                        }
+                    )
+                else:
+                    raise EngineTypeNotFoundException(domain.domain_name)
+            else:
+                domains.append(
+                    {
+                        "DomainName": domain.domain_name,
+                        "EngineType": domain.engine_version.split("_")[0],
+                    }
+                )
+        return domains
+
 
 opensearch_backends = BackendDict(OpenSearchServiceBackend, "opensearch")
diff --git a/moto/opensearch/responses.py b/moto/opensearch/responses.py
index 2d189ffac..2d3bbf088 100644
--- a/moto/opensearch/responses.py
+++ b/moto/opensearch/responses.py
@@ -2,6 +2,7 @@
 import json
 
 from moto.core.responses import BaseResponse
+
 from .models import opensearch_backends, OpenSearchServiceBackend
 
 
@@ -138,3 +139,10 @@ class OpenSearchServiceResponse(BaseResponse):
         tag_keys = self._get_param("TagKeys")
         self.opensearch_backend.remove_tags(arn, tag_keys)
         return "{}"
+
+    def list_domain_names(self) -> str:
+        engine_type = self._get_param("engineType")
+        domain_names = self.opensearch_backend.list_domain_names(
+            engine_type=engine_type,
+        )
+        return json.dumps(dict(DomainNames=domain_names))


Test patch:
diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py
index 667c0dfa4..ec5f1982f 100644
--- a/tests/test_dynamodb/test_dynamodb.py
+++ b/tests/test_dynamodb/test_dynamodb.py
@@ -5745,11 +5745,19 @@ def test_projection_expression_with_binary_attr():
     )
     table = dynamo_resource.Table("test")
     table.put_item(Item={"pk": "pk", "sk": "sk", "key": b"value\xbf"})
-    assert table.get_item(
+
+    item = table.get_item(
         Key={"pk": "pk", "sk": "sk"},
         ExpressionAttributeNames={"#key": "key"},
         ProjectionExpression="#key",
-    )["Item"] == {"key": Binary(b"value\xbf")}
+    )["Item"]
+    assert item == {"key": Binary(b"value\xbf")}
+
+    item = table.scan()["Items"][0]
+    assert item["key"] == Binary(b"value\xbf")
+
+    item = table.query(KeyConditionExpression=Key("pk").eq("pk"))["Items"][0]
+    assert item["key"] == Binary(b"value\xbf")
 
 
 @mock_dynamodb
diff --git a/tests/test_dynamodb/test_dynamodb_batch_get_item.py b/tests/test_dynamodb/test_dynamodb_batch_get_item.py
index 0bb678ba5..fa75e92ac 100644
--- a/tests/test_dynamodb/test_dynamodb_batch_get_item.py
+++ b/tests/test_dynamodb/test_dynamodb_batch_get_item.py
@@ -14,7 +14,7 @@ def _create_user_table():
         ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
     )
     client.put_item(
-        TableName="users", Item={"username": {"S": "user1"}, "foo": {"S": "bar"}}
+        TableName="users", Item={"username": {"S": "user1"}, "binaryfoo": {"B": b"bar"}}
     )
     client.put_item(
         TableName="users", Item={"username": {"S": "user2"}, "foo": {"S": "bar"}}
@@ -42,11 +42,9 @@ def test_batch_items_returns_all():
         }
     )["Responses"]["users"]
     assert len(returned_items) == 3
-    assert [item["username"]["S"] for item in returned_items] == [
-        "user1",
-        "user2",
-        "user3",
-    ]
+    assert {"username": {"S": "user1"}, "binaryfoo": {"B": b"bar"}} in returned_items
+    assert {"username": {"S": "user2"}, "foo": {"S": "bar"}} in returned_items
+    assert {"username": {"S": "user3"}, "foo": {"S": "bar"}} in returned_items
 
 
 @mock_dynamodb
@@ -137,12 +135,10 @@ def test_batch_items_with_basic_projection_expression():
         }
     )["Responses"]["users"]
 
-    assert [item["username"]["S"] for item in returned_items] == [
-        "user1",
-        "user2",
-        "user3",
-    ]
-    assert [item["foo"]["S"] for item in returned_items] == ["bar", "bar", "bar"]
+    assert len(returned_items) == 3
+    assert {"username": {"S": "user1"}, "binaryfoo": {"B": b"bar"}} in returned_items
+    assert {"username": {"S": "user2"}, "foo": {"S": "bar"}} in returned_items
+    assert {"username": {"S": "user3"}, "foo": {"S": "bar"}} in returned_items
 
 
 @mock_dynamodb
@@ -165,12 +161,9 @@ def test_batch_items_with_basic_projection_expression_and_attr_expression_names(
     )["Responses"]["users"]
 
     assert len(returned_items) == 3
-    assert [item["username"]["S"] for item in returned_items] == [
-        "user1",
-        "user2",
-        "user3",
-    ]
-    assert [item.get("foo") for item in returned_items] == [None, None, None]
+    assert {"username": {"S": "user1"}} in returned_items
+    assert {"username": {"S": "user2"}} in returned_items
+    assert {"username": {"S": "user3"}} in returned_items
 
 
 @mock_dynamodb
diff --git a/tests/test_opensearch/test_opensearch.py b/tests/test_opensearch/test_opensearch.py
index 38609040c..380e018bd 100644
--- a/tests/test_opensearch/test_opensearch.py
+++ b/tests/test_opensearch/test_opensearch.py
@@ -1,9 +1,10 @@
 import boto3
 import pytest
-
 from botocore.exceptions import ClientError
+
 from moto import mock_opensearch
 
+
 # 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
 
@@ -139,3 +140,67 @@ def test_update_domain_config():
             "CustomEndpointEnabled": False,
         }
     }
+
+
+@mock_opensearch
+def test_list_domain_names():
+    client = boto3.client("opensearch", region_name="ap-southeast-1")
+
+    test_domain_names_list_exist = False
+
+    opensearch_domain_name = "testdn"
+    opensearch_engine_version = "OpenSearch_1.0"
+    client.create_domain(
+        DomainName=opensearch_domain_name, EngineVersion=opensearch_engine_version
+    )
+
+    resp = client.list_domain_names()
+    domain_names = resp["DomainNames"]
+
+    for domain_name in domain_names:
+        if domain_name["DomainName"] == opensearch_domain_name:
+            test_domain_names_list_exist = True
+
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert test_domain_names_list_exist
+
+
+@mock_opensearch
+def test_list_filtered_domain_names():
+    client = boto3.client("opensearch", region_name="ap-southeast-1")
+
+    test_domain_names_list_exist = False
+
+    opensearch_domain_name = "testdn"
+    opensearch_engine_version = "OpenSearch_1.0"
+    client.create_domain(
+        DomainName=opensearch_domain_name, EngineVersion=opensearch_engine_version
+    )
+
+    resp = client.list_domain_names(EngineType="OpenSearch")
+    domain_names = resp["DomainNames"]
+
+    for domain_name in domain_names:
+        if domain_name["DomainName"] == opensearch_domain_name:
+            if domain_name["EngineType"] == opensearch_engine_version.split("_")[0]:
+                test_domain_names_list_exist = True
+
+    assert resp["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert test_domain_names_list_exist
+
+
+@mock_opensearch
+def test_list_unknown_domain_names_engine_type():
+    client = boto3.client("opensearch", region_name="ap-southeast-1")
+
+    opensearch_domain_name = "testdn"
+    opensearch_engine_version = "OpenSearch_1.0"
+    client.create_domain(
+        DomainName=opensearch_domain_name, EngineVersion=opensearch_engine_version
+    )
+
+    with pytest.raises(ClientError) as exc:
+        client.list_domain_names(EngineType="unknown")
+    err = exc.value.response["Error"]
+    assert err["Code"] == "EngineTypeNotFoundException"
+    assert err["Message"] == "Engine Type not found: testdn"