1"""
2    Tests for salt.modules.boto3_elasticsearch
3"""
4
5
6import datetime
7import random
8import string
9import textwrap
10
11import salt.loader
12import salt.modules.boto3_elasticsearch as boto3_elasticsearch
13from salt.utils.versions import LooseVersion
14from tests.support.mixins import LoaderModuleMockMixin
15from tests.support.mock import MagicMock, patch
16from tests.support.unit import TestCase, skipIf
17
18try:
19    import boto3
20    from botocore.exceptions import ClientError
21
22    HAS_BOTO3 = True
23except ImportError:
24    HAS_BOTO3 = False
25
26# the boto3_elasticsearch module relies on the connect_to_region() method
27# which was added in boto 2.8.0
28# https://github.com/boto/boto/commit/33ac26b416fbb48a60602542b4ce15dcc7029f12
29REQUIRED_BOTO3_VERSION = "1.2.1"
30
31
32def __virtual__():
33    """
34    Returns True/False boolean depending on if Boto3 is installed and correct
35    version.
36    """
37    if not HAS_BOTO3:
38        return False
39    if LooseVersion(boto3.__version__) < LooseVersion(REQUIRED_BOTO3_VERSION):
40        return (
41            False,
42            "The boto3 module must be greater or equal to version {}".format(
43                REQUIRED_BOTO3_VERSION
44            ),
45        )
46    return True
47
48
49REGION = "us-east-1"
50ACCESS_KEY = "GKTADJGHEIQSXMKKRBJ08H"
51SECRET_KEY = "askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs"
52CONN_PARAMETERS = {
53    "region": REGION,
54    "key": ACCESS_KEY,
55    "keyid": SECRET_KEY,
56    "profile": {},
57}
58ERROR_MESSAGE = (
59    "An error occurred ({}) when calling the {} operation: Test-defined error"
60)
61ERROR_CONTENT = {"Error": {"Code": 101, "Message": "Test-defined error"}}
62NOT_FOUND_ERROR = ClientError(
63    {"Error": {"Code": "ResourceNotFoundException", "Message": "Test-defined error"}},
64    "msg",
65)
66DOMAIN_RET = {
67    "DomainId": "accountno/testdomain",
68    "DomainName": "testdomain",
69    "ARN": "arn:aws:es:region:accountno:domain/testdomain",
70    "Created": True,
71    "Deleted": False,
72    "Endpoints": {"vpc": "vpc-testdomain-1234567890.region.es.amazonaws.com"},
73    "Processing": False,
74    "UpgradeProcessing": False,
75    "ElasticsearchVersion": "6.3",
76    "ElasticsearchClusterConfig": {
77        "InstanceType": "t2.medium.elasticsearch",
78        "InstanceCount": 1,
79        "DedicatedMasterEnabled": False,
80        "ZoneAwarenessEnabled": False,
81    },
82    "EBSOptions": {
83        "EBSEnabled": True,
84        "VolumeType": "gp2",
85        "VolumeSize": 123,
86        "Iops": 12,
87    },
88    "AccessPolicies": textwrap.dedent(
89        """
90        {"Version":"2012-10-17","Statement":[{"Effect":"Allow",
91        "Principal":{"AWS":"*"},"Action":"es:*",
92        "Resource":"arn:aws:es:region:accountno:domain/testdomain/*"}]}"""
93    ),
94    "SnapshotOptions": {"AutomatedSnapshotStartHour": 1},
95    "VPCOptions": {
96        "VPCId": "vpc-12345678",
97        "SubnetIds": ["subnet-deadbeef"],
98        "AvailabilityZones": ["regiona"],
99        "SecurityGroupIds": ["sg-87654321"],
100    },
101    "CognitoOptions": {"Enabled": False},
102    "EncryptionAtRestOptions": {"Enabled": False},
103    "NodeToNodeEncryptionOptions": {"Enabled": False},
104    "AdvancedOptions": {"rest.action.multi.allow_explicit_index": "true"},
105    "ServiceSoftwareOptions": {
106        "CurrentVersion": "R20190221-P1",
107        "NewVersion": "R20190418",
108        "UpdateAvailable": True,
109        "Cancellable": False,
110        "UpdateStatus": "ELIGIBLE",
111        "Description": (
112            "A newer release R20190418 is available. This release "
113            "will be automatically deployed after somedate"
114        ),
115        "AutomatedUpdateDate": None,
116    },
117}
118
119
120@skipIf(HAS_BOTO3 is False, "The boto module must be installed.")
121@skipIf(
122    LooseVersion(boto3.__version__) < LooseVersion(REQUIRED_BOTO3_VERSION),
123    "The boto3 module must be greater or equal to version {}".format(
124        REQUIRED_BOTO3_VERSION
125    ),
126)
127class Boto3ElasticsearchTestCase(TestCase, LoaderModuleMockMixin):
128    """
129    TestCase for salt.modules.boto3_elasticsearch module
130    """
131
132    conn = None
133
134    def setup_loader_modules(self):
135        self.opts = salt.config.DEFAULT_MINION_OPTS.copy()
136        utils = salt.loader.utils(
137            self.opts,
138            whitelist=["boto3", "args", "systemd", "path", "platform"],
139            context={},
140        )
141        return {boto3_elasticsearch: {"__utils__": utils}}
142
143    def setUp(self):
144        super().setUp()
145        boto3_elasticsearch.__init__(self.opts)
146        del self.opts
147
148        # Set up MagicMock to replace the boto3 session
149        # connections keep getting cached from prior tests, can't find the
150        # correct context object to clear it. So randomize the cache key, to prevent any
151        # cache hits
152        CONN_PARAMETERS["key"] = "".join(
153            random.choice(string.ascii_lowercase + string.digits) for _ in range(50)
154        )
155
156        self.conn = MagicMock()
157        self.addCleanup(delattr, self, "conn")
158        self.patcher = patch("boto3.session.Session")
159        self.addCleanup(self.patcher.stop)
160        self.addCleanup(delattr, self, "patcher")
161        mock_session = self.patcher.start()
162        session_instance = mock_session.return_value
163        session_instance.configure_mock(client=MagicMock(return_value=self.conn))
164        self.paginator = MagicMock()
165        self.addCleanup(delattr, self, "paginator")
166        self.conn.configure_mock(get_paginator=MagicMock(return_value=self.paginator))
167
168    def test_describe_elasticsearch_domain_positive(self):
169        """
170        Test that when describing a domain when the domain actually exists,
171        the .exists method returns a dict with 'result': True
172        and 'response' with the domain status information.
173        """
174        # The patch below is not neccesary per se,
175        # as .exists returns positive as long as no exception is raised.
176        with patch.object(
177            self.conn,
178            "describe_elasticsearch_domain",
179            return_value={"DomainStatus": DOMAIN_RET},
180        ):
181            self.assertEqual(
182                boto3_elasticsearch.describe_elasticsearch_domain(
183                    domain_name="testdomain", **CONN_PARAMETERS
184                ),
185                {"result": True, "response": DOMAIN_RET},
186            )
187
188    def test_describe_elasticsearch_domain_error(self):
189        """
190        Test that when describing a domain when the domain does not exist,
191        the .exists method returns a dict with 'result': False
192        and 'error' with boto's ResourceNotFoundException.
193        """
194        with patch.object(
195            self.conn, "describe_elasticsearch_domain", side_effect=NOT_FOUND_ERROR
196        ):
197            result = boto3_elasticsearch.describe_elasticsearch_domain(
198                domain_name="testdomain", **CONN_PARAMETERS
199            )
200            self.assertEqual(
201                result.get("error", ""),
202                ERROR_MESSAGE.format("ResourceNotFoundException", "msg"),
203            )
204            self.assertFalse(result["result"])
205
206    def test_create_elasticsearch_domain_positive(self):
207        """
208        Test that when creating a domain, and it succeeds,
209        the .create method returns a dict with 'result': True
210        and 'response' with the newly created domain's status information.
211        """
212        with patch.object(
213            self.conn,
214            "create_elasticsearch_domain",
215            return_value={"DomainStatus": DOMAIN_RET},
216        ):
217            kwargs = {
218                "elasticsearch_version": DOMAIN_RET["ElasticsearchVersion"],
219                "elasticsearch_cluster_config": DOMAIN_RET[
220                    "ElasticsearchClusterConfig"
221                ],
222                "ebs_options": DOMAIN_RET["EBSOptions"],
223                "access_policies": DOMAIN_RET["AccessPolicies"],
224                "snapshot_options": DOMAIN_RET["SnapshotOptions"],
225                "vpc_options": DOMAIN_RET["VPCOptions"],
226                "cognito_options": DOMAIN_RET["CognitoOptions"],
227                "encryption_at_rest_options": DOMAIN_RET["EncryptionAtRestOptions"],
228                "advanced_options": DOMAIN_RET["AdvancedOptions"],
229            }
230            kwargs.update(CONN_PARAMETERS)
231            self.assertEqual(
232                boto3_elasticsearch.create_elasticsearch_domain(
233                    domain_name="testdomain", **kwargs
234                ),
235                {"result": True, "response": DOMAIN_RET},
236            )
237
238    def test_create_elasticsearch_domain_error(self):
239        """
240        Test that when creating a domain, and boto3 returns an error,
241        the .create method returns a dict with 'result': False
242        and 'error' with the error reported by boto3.
243        """
244        with patch.object(
245            self.conn,
246            "create_elasticsearch_domain",
247            side_effect=ClientError(ERROR_CONTENT, "create_domain"),
248        ):
249            kwargs = {
250                "elasticsearch_version": DOMAIN_RET["ElasticsearchVersion"],
251                "elasticsearch_cluster_config": DOMAIN_RET[
252                    "ElasticsearchClusterConfig"
253                ],
254                "ebs_options": DOMAIN_RET["EBSOptions"],
255                "access_policies": DOMAIN_RET["AccessPolicies"],
256                "snapshot_options": DOMAIN_RET["SnapshotOptions"],
257                "vpc_options": DOMAIN_RET["VPCOptions"],
258                "cognito_options": DOMAIN_RET["CognitoOptions"],
259                "encryption_at_rest_options": DOMAIN_RET["EncryptionAtRestOptions"],
260                "advanced_options": DOMAIN_RET["AdvancedOptions"],
261            }
262            kwargs.update(CONN_PARAMETERS)
263            result = boto3_elasticsearch.create_elasticsearch_domain(
264                "testdomain", **kwargs
265            )
266            self.assertEqual(
267                result.get("error", ""), ERROR_MESSAGE.format(101, "create_domain")
268            )
269
270    def test_delete_domain_positive(self):
271        """
272        Test that when deleting a domain, and it succeeds,
273        the .delete method returns {'result': True}.
274        """
275        with patch.object(self.conn, "delete_elasticsearch_domain"):
276            self.assertEqual(
277                boto3_elasticsearch.delete_elasticsearch_domain(
278                    "testdomain", **CONN_PARAMETERS
279                ),
280                {"result": True},
281            )
282
283    def test_delete_domain_error(self):
284        """
285        Test that when deleting a domain, and boto3 returns an error,
286        the .delete method returns {'result': False, 'error' :'the error'}.
287        """
288        with patch.object(
289            self.conn,
290            "delete_elasticsearch_domain",
291            side_effect=ClientError(ERROR_CONTENT, "delete_domain"),
292        ):
293            result = boto3_elasticsearch.delete_elasticsearch_domain(
294                "testdomain", **CONN_PARAMETERS
295            )
296            self.assertFalse(result["result"])
297            self.assertEqual(
298                result.get("error", ""), ERROR_MESSAGE.format(101, "delete_domain")
299            )
300
301    def test_update_domain_positive(self):
302        """
303        Test that when updating a domain succeeds, the .update method returns {'result': True}.
304        """
305        with patch.object(
306            self.conn,
307            "update_elasticsearch_domain_config",
308            return_value={"DomainConfig": DOMAIN_RET},
309        ):
310            kwargs = {
311                "elasticsearch_cluster_config": DOMAIN_RET[
312                    "ElasticsearchClusterConfig"
313                ],
314                "ebs_options": DOMAIN_RET["EBSOptions"],
315                "snapshot_options": DOMAIN_RET["SnapshotOptions"],
316                "vpc_options": DOMAIN_RET["VPCOptions"],
317                "cognito_options": DOMAIN_RET["CognitoOptions"],
318                "advanced_options": DOMAIN_RET["AdvancedOptions"],
319                "access_policies": DOMAIN_RET["AccessPolicies"],
320                "log_publishing_options": {},
321            }
322
323            kwargs.update(CONN_PARAMETERS)
324            self.assertEqual(
325                boto3_elasticsearch.update_elasticsearch_domain_config(
326                    "testdomain", **kwargs
327                ),
328                {"result": True, "response": DOMAIN_RET},
329            )
330
331    def test_update_domain_error(self):
332        """
333        Test that when updating a domain fails, and boto3 returns an error,
334        the .update method returns the error.
335        """
336        with patch.object(
337            self.conn,
338            "update_elasticsearch_domain_config",
339            side_effect=ClientError(ERROR_CONTENT, "update_domain"),
340        ):
341            kwargs = {
342                "elasticsearch_cluster_config": DOMAIN_RET[
343                    "ElasticsearchClusterConfig"
344                ],
345                "ebs_options": DOMAIN_RET["EBSOptions"],
346                "snapshot_options": DOMAIN_RET["SnapshotOptions"],
347                "vpc_options": DOMAIN_RET["VPCOptions"],
348                "cognito_options": DOMAIN_RET["CognitoOptions"],
349                "advanced_options": DOMAIN_RET["AdvancedOptions"],
350                "access_policies": DOMAIN_RET["AccessPolicies"],
351                "log_publishing_options": {},
352            }
353            kwargs.update(CONN_PARAMETERS)
354            result = boto3_elasticsearch.update_elasticsearch_domain_config(
355                "testdomain", **kwargs
356            )
357            self.assertEqual(
358                result.get("error", ""), ERROR_MESSAGE.format(101, "update_domain")
359            )
360
361    def test_add_tags_positive(self):
362        """
363        Test that when adding tags is successful, the .add_tags method returns {'result': True}.
364        """
365        with patch.object(
366            self.conn,
367            "describe_elasticsearch_domain",
368            return_value={"DomainStatus": DOMAIN_RET},
369        ):
370            self.assertEqual(
371                boto3_elasticsearch.add_tags(
372                    "testdomain", tags={"foo": "bar", "baz": "qux"}, **CONN_PARAMETERS
373                ),
374                {"result": True},
375            )
376
377    def test_add_tags_default(self):
378        """
379        Test that when tags are not provided, no error is raised.
380        """
381        with patch.object(
382            self.conn,
383            "describe_elasticsearch_domain",
384            return_value={"DomainStatus": DOMAIN_RET},
385        ):
386            self.assertEqual(
387                boto3_elasticsearch.add_tags("testdomain", **CONN_PARAMETERS),
388                {"result": True},
389            )
390
391    def test_add_tags_error(self):
392        """
393        Test that when adding tags fails, and boto3 returns an error,
394        the .add_tags function returns {'tagged': False, 'error': 'the error'}.
395        """
396        with patch.object(
397            self.conn, "add_tags", side_effect=ClientError(ERROR_CONTENT, "add_tags")
398        ), patch.object(
399            self.conn,
400            "describe_elasticsearch_domain",
401            return_value={"DomainStatus": DOMAIN_RET},
402        ):
403            result = boto3_elasticsearch.add_tags(
404                "testdomain", tags={"foo": "bar", "baz": "qux"}, **CONN_PARAMETERS
405            )
406            self.assertFalse(result["result"])
407            self.assertEqual(
408                result.get("error", ""), ERROR_MESSAGE.format(101, "add_tags")
409            )
410
411    def test_remove_tags_positive(self):
412        """
413        Test that when removing tags is successful, the .remove_tags method returns {'tagged': True}.
414        """
415        with patch.object(
416            self.conn,
417            "describe_elasticsearch_domain",
418            return_value={"DomainStatus": DOMAIN_RET},
419        ):
420            self.assertEqual(
421                boto3_elasticsearch.remove_tags(
422                    tag_keys=["foo", "bar"], domain_name="testdomain", **CONN_PARAMETERS
423                ),
424                {"result": True},
425            )
426
427    def test_remove_tag_error(self):
428        """
429        Test that when removing tags fails, and boto3 returns an error,
430        the .remove_tags method returns {'tagged': False, 'error': 'the error'}.
431        """
432        with patch.object(
433            self.conn,
434            "remove_tags",
435            side_effect=ClientError(ERROR_CONTENT, "remove_tags"),
436        ), patch.object(
437            self.conn,
438            "describe_elasticsearch_domain",
439            return_value={"DomainStatus": DOMAIN_RET},
440        ):
441            result = boto3_elasticsearch.remove_tags(
442                tag_keys=["foo", "bar"], domain_name="testdomain", **CONN_PARAMETERS
443            )
444            self.assertFalse(result["result"])
445            self.assertEqual(
446                result.get("error", ""), ERROR_MESSAGE.format(101, "remove_tags")
447            )
448
449    def test_list_tags_positive(self):
450        """
451        Test that when listing tags is successful,
452        the .list_tags method returns a dict with key 'tags'.
453        Also test that the tags returned are manipulated properly (i.e. transformed
454        into a dict with tags).
455        """
456        with patch.object(
457            self.conn,
458            "describe_elasticsearch_domain",
459            return_value={"DomainStatus": DOMAIN_RET},
460        ), patch.object(
461            self.conn,
462            "list_tags",
463            return_value={"TagList": [{"Key": "foo", "Value": "bar"}]},
464        ):
465            result = boto3_elasticsearch.list_tags(
466                domain_name="testdomain", **CONN_PARAMETERS
467            )
468            self.assertEqual(result, {"result": True, "response": {"foo": "bar"}})
469
470    def test_list_tags_error(self):
471        """
472        Test that when listing tags causes boto3 to return an error,
473        the .list_tags method returns the error.
474        """
475        with patch.object(
476            self.conn, "list_tags", side_effect=ClientError(ERROR_CONTENT, "list_tags")
477        ), patch.object(
478            self.conn,
479            "describe_elasticsearch_domain",
480            return_value={"DomainStatus": DOMAIN_RET},
481        ):
482            result = boto3_elasticsearch.list_tags(
483                domain_name="testdomain", **CONN_PARAMETERS
484            )
485            self.assertFalse(result["result"])
486            self.assertEqual(
487                result.get("error", ""), ERROR_MESSAGE.format(101, "list_tags")
488            )
489
490    def test_cancel_elasticsearch_service_software_update_positive(self):
491        """
492        Test that when calling cancel_elasticsearch_service_software_update and
493        it is successful, it returns {'result': True}.
494        """
495        retval = {
496            "ServiceSoftwareOptions": {
497                "CurrentVersion": "string",
498                "NewVersion": "string",
499                "UpdateAvailable": True,
500                "Cancellable": True,
501                "UpdateStatus": "ELIGIBLE",
502                "Description": "string",
503                "AutomatedUpdateDate": datetime.datetime(2015, 1, 1),
504            }
505        }
506        with patch.object(
507            self.conn,
508            "cancel_elasticsearch_service_software_update",
509            return_value=retval,
510        ):
511            result = boto3_elasticsearch.cancel_elasticsearch_service_software_update(
512                domain_name="testdomain", **CONN_PARAMETERS
513            )
514            self.assertEqual(result, {"result": True})
515
516    def test_cancel_elasticsearch_service_software_update_error(self):
517        """
518        Test that when calling cancel_elasticsearch_service_software_update and
519        boto3 returns an error, it returns {'result': False, 'error': 'the error'}.
520        """
521        with patch.object(
522            self.conn,
523            "cancel_elasticsearch_service_software_update",
524            side_effect=ClientError(
525                ERROR_CONTENT, "cancel_elasticsearch_service_software_update"
526            ),
527        ):
528            result = boto3_elasticsearch.cancel_elasticsearch_service_software_update(
529                domain_name="testdomain", **CONN_PARAMETERS
530            )
531            self.assertFalse(result["result"])
532            self.assertEqual(
533                result.get("error", ""),
534                ERROR_MESSAGE.format(
535                    101, "cancel_elasticsearch_service_software_update"
536                ),
537            )
538
539    def test_delete_elasticsearch_service_role_positive(self):
540        """
541        Test that when calling delete_elasticsearch_service_role and
542        it is successful, it returns {'result': True}.
543        """
544        with patch.object(
545            self.conn, "delete_elasticsearch_service_role", return_value=None
546        ):
547            result = boto3_elasticsearch.delete_elasticsearch_service_role(
548                **CONN_PARAMETERS
549            )
550            self.assertEqual(result, {"result": True})
551
552    def test_delete_elasticsearch_service_role_error(self):
553        """
554        Test that when calling delete_elasticsearch_service_role and boto3 returns
555        an error, it returns {'result': False, 'error': 'the error'}.
556        """
557        with patch.object(
558            self.conn,
559            "delete_elasticsearch_service_role",
560            side_effect=ClientError(ERROR_CONTENT, "delete_elasticsearch_service_role"),
561        ):
562            result = boto3_elasticsearch.delete_elasticsearch_service_role(
563                **CONN_PARAMETERS
564            )
565            self.assertFalse(result["result"])
566            self.assertEqual(
567                result.get("error", ""),
568                ERROR_MESSAGE.format(101, "delete_elasticsearch_service_role"),
569            )
570
571    def test_describe_elasticsearch_domain_config_positive(self):
572        """
573        Test that when calling describe_elasticsearch_domain_config and
574        it is successful, it returns {'result': True}.
575        """
576        with patch.object(
577            self.conn,
578            "describe_elasticsearch_domain_config",
579            return_value={"DomainConfig": DOMAIN_RET},
580        ):
581            self.assertEqual(
582                boto3_elasticsearch.describe_elasticsearch_domain_config(
583                    "testdomain", **CONN_PARAMETERS
584                ),
585                {"result": True, "response": DOMAIN_RET},
586            )
587
588    def test_describe_elasticsearch_domain_config_error(self):
589        """
590        Test that when calling describe_elasticsearch_domain_config and boto3 returns
591        an error, it returns {'result': False, 'error': 'the error'}.
592        """
593        with patch.object(
594            self.conn,
595            "describe_elasticsearch_domain_config",
596            side_effect=ClientError(
597                ERROR_CONTENT, "describe_elasticsearch_domain_config"
598            ),
599        ):
600            result = boto3_elasticsearch.describe_elasticsearch_domain_config(
601                domain_name="testdomain", **CONN_PARAMETERS
602            )
603            self.assertFalse(result["result"])
604            self.assertEqual(
605                result.get("error", ""),
606                ERROR_MESSAGE.format(101, "describe_elasticsearch_domain_config"),
607            )
608
609    def test_describe_elasticsearch_domains_positive(self):
610        """
611        Test that when calling describe_elasticsearch_domains and it is successful,
612        it returns {'result': True, 'response': some_data}.
613        """
614        with patch.object(
615            self.conn,
616            "describe_elasticsearch_domains",
617            return_value={"DomainStatusList": [DOMAIN_RET]},
618        ):
619            self.assertEqual(
620                boto3_elasticsearch.describe_elasticsearch_domains(
621                    domain_names=["test_domain"], **CONN_PARAMETERS
622                ),
623                {"result": True, "response": [DOMAIN_RET]},
624            )
625
626    def test_describe_elasticsearch_domains_error(self):
627        """
628        Test that when calling describe_elasticsearch_domains and boto3 returns
629        an error, it returns {'result': False, 'error': 'the error'}.
630        """
631        with patch.object(
632            self.conn,
633            "describe_elasticsearch_domains",
634            side_effect=ClientError(ERROR_CONTENT, "describe_elasticsearch_domains"),
635        ):
636            result = boto3_elasticsearch.describe_elasticsearch_domains(
637                domain_names=["testdomain"], **CONN_PARAMETERS
638            )
639            self.assertFalse(result["result"])
640            self.assertEqual(
641                result.get("error", ""),
642                ERROR_MESSAGE.format(101, "describe_elasticsearch_domains"),
643            )
644
645    def test_describe_elasticsearch_instance_type_limits_positive(self):
646        """
647        Test that when calling describe_elasticsearch_instance_type_limits and
648        it succeeds, it returns {'result': True, 'response' some_value}.
649        """
650        ret_val = {
651            "LimitsByRole": {
652                "string": {
653                    "StorageTypes": [
654                        {
655                            "StorageTypeName": "string",
656                            "StorageSubTypeName": "string",
657                            "StorageTypeLimits": [
658                                {"LimitName": "string", "LimitValues": ["string"]}
659                            ],
660                        }
661                    ],
662                    "InstanceLimits": {
663                        "InstanceCountLimits": {
664                            "MinimumInstanceCount": 123,
665                            "MaximumInstanceCount": 123,
666                        }
667                    },
668                    "AdditionalLimits": [
669                        {"LimitName": "string", "LimitValues": ["string"]}
670                    ],
671                }
672            }
673        }
674        with patch.object(
675            self.conn,
676            "describe_elasticsearch_instance_type_limits",
677            return_value=ret_val,
678        ):
679            self.assertEqual(
680                boto3_elasticsearch.describe_elasticsearch_instance_type_limits(
681                    domain_name="testdomain",
682                    instance_type="foo",
683                    elasticsearch_version="1.0",
684                    **CONN_PARAMETERS
685                ),
686                {"result": True, "response": ret_val["LimitsByRole"]},
687            )
688
689    def test_describe_elasticsearch_instance_type_limits_error(self):
690        """
691        Test that when calling describe_elasticsearch_instance_type_limits and boto3 returns
692        an error, it returns {'result': False, 'error': 'the error'}.
693        """
694        with patch.object(
695            self.conn,
696            "describe_elasticsearch_instance_type_limits",
697            side_effect=ClientError(
698                ERROR_CONTENT, "describe_elasticsearch_instance_type_limits"
699            ),
700        ):
701            result = boto3_elasticsearch.describe_elasticsearch_instance_type_limits(
702                domain_name="testdomain",
703                instance_type="foo",
704                elasticsearch_version="1.0",
705                **CONN_PARAMETERS
706            )
707            self.assertFalse(result["result"])
708            self.assertEqual(
709                result.get("error", ""),
710                ERROR_MESSAGE.format(
711                    101, "describe_elasticsearch_instance_type_limits"
712                ),
713            )
714
715    def test_describe_reserved_elasticsearch_instance_offerings_positive(self):
716        """
717        Test that when calling describe_reserved_elasticsearch_instance_offerings
718        and it succeeds, it returns {'result': True, 'response': some_value}.
719        """
720        ret_val = {
721            "NextToken": "string",
722            "ReservedElasticsearchInstanceOfferings": [
723                {
724                    "ReservedElasticsearchInstanceOfferingId": "string",
725                    "ElasticsearchInstanceType": "t2.medium.elasticsearch",
726                    "Duration": 123,
727                    "FixedPrice": 123.0,
728                    "UsagePrice": 123.0,
729                    "CurrencyCode": "string",
730                    "PaymentOption": "NO_UPFRONT",
731                    "RecurringCharges": [
732                        {
733                            "RecurringChargeAmount": 123.0,
734                            "RecurringChargeFrequency": "string",
735                        }
736                    ],
737                }
738            ],
739        }
740        with patch.object(self.paginator, "paginate", return_value=[ret_val]):
741            self.assertEqual(
742                boto3_elasticsearch.describe_reserved_elasticsearch_instance_offerings(
743                    reserved_elasticsearch_instance_offering_id="foo", **CONN_PARAMETERS
744                ),
745                {
746                    "result": True,
747                    "response": ret_val["ReservedElasticsearchInstanceOfferings"],
748                },
749            )
750
751    def test_describe_reserved_elasticsearch_instance_offerings_error(self):
752        """
753        Test that when calling describe_reserved_elasticsearch_instance_offerings
754        and boto3 returns an error, it returns {'result': False, 'error': 'the error'}.
755        """
756        with patch.object(
757            self.paginator,
758            "paginate",
759            side_effect=ClientError(
760                ERROR_CONTENT, "describe_reserved_elasticsearch_instance_offerings"
761            ),
762        ):
763            result = (
764                boto3_elasticsearch.describe_reserved_elasticsearch_instance_offerings(
765                    reserved_elasticsearch_instance_offering_id="foo", **CONN_PARAMETERS
766                )
767            )
768            self.assertFalse(result["result"])
769            self.assertEqual(
770                result.get("error", ""),
771                ERROR_MESSAGE.format(
772                    101, "describe_reserved_elasticsearch_instance_offerings"
773                ),
774            )
775
776    def test_describe_reserved_elasticsearch_instances_positive(self):
777        """
778        Test that when calling describe_reserved_elasticsearch_instances and it
779        succeeds, it returns {'result': True, 'response': some_value}.
780        """
781        ret_val = {
782            "NextToken": "string",
783            "ReservedElasticsearchInstances": [
784                {
785                    "ReservationName": "string",
786                    "ReservedElasticsearchInstanceId": "string",
787                    "ReservedElasticsearchInstanceOfferingId": "string",
788                    "ElasticsearchInstanceType": "t2.medium.elasticsearch",
789                    "StartTime": datetime.datetime(2015, 1, 1),
790                    "Duration": 123,
791                    "FixedPrice": 123.0,
792                    "UsagePrice": 123.0,
793                    "CurrencyCode": "string",
794                    "ElasticsearchInstanceCount": 123,
795                    "State": "string",
796                    "PaymentOption": "ALL_UPFRONT",
797                    "RecurringCharges": [
798                        {
799                            "RecurringChargeAmount": 123.0,
800                            "RecurringChargeFrequency": "string",
801                        },
802                    ],
803                },
804            ],
805        }
806        with patch.object(self.paginator, "paginate", return_value=[ret_val]):
807            self.assertEqual(
808                boto3_elasticsearch.describe_reserved_elasticsearch_instances(
809                    reserved_elasticsearch_instance_id="foo", **CONN_PARAMETERS
810                ),
811                {"result": True, "response": ret_val["ReservedElasticsearchInstances"]},
812            )
813
814    def test_describe_reserved_elasticsearch_instances_error(self):
815        """
816        Test that when calling describe_reserved_elasticsearch_instances and boto3
817        returns an error, it returns {'result': False, 'error': 'the error'}.
818        """
819        with patch.object(
820            self.paginator,
821            "paginate",
822            side_effect=ClientError(
823                ERROR_CONTENT, "describe_reserved_elasticsearch_instances"
824            ),
825        ):
826            result = boto3_elasticsearch.describe_reserved_elasticsearch_instances(
827                reserved_elasticsearch_instance_id="foo", **CONN_PARAMETERS
828            )
829            self.assertFalse(result["result"])
830            self.assertEqual(
831                result.get("error", ""),
832                ERROR_MESSAGE.format(101, "describe_reserved_elasticsearch_instances"),
833            )
834
835    def test_get_compatible_elasticsearch_versions_positive(self):
836        """
837        Test that when calling get_compatible_elasticsearch_versions and it
838        succeeds, it returns {'result': True, 'response': some_value}.
839        """
840        ret_val = {
841            "CompatibleElasticsearchVersions": [
842                {"SourceVersion": "string", "TargetVersions": ["string"]}
843            ]
844        }
845        with patch.object(
846            self.conn, "get_compatible_elasticsearch_versions", return_value=ret_val
847        ):
848            self.assertEqual(
849                boto3_elasticsearch.get_compatible_elasticsearch_versions(
850                    domain_name="testdomain", **CONN_PARAMETERS
851                ),
852                {
853                    "result": True,
854                    "response": ret_val["CompatibleElasticsearchVersions"],
855                },
856            )
857
858    def test_get_compatible_elasticsearch_versions_error(self):
859        """
860        Test that when calling get_compatible_elasticsearch_versions and boto3
861        returns an error, it returns {'result': False, 'error': 'the error'}.
862        """
863        with patch.object(
864            self.conn,
865            "get_compatible_elasticsearch_versions",
866            side_effect=ClientError(
867                ERROR_CONTENT, "get_compatible_elasticsearch_versions"
868            ),
869        ):
870            result = boto3_elasticsearch.get_compatible_elasticsearch_versions(
871                domain_name="testdomain", **CONN_PARAMETERS
872            )
873            self.assertFalse(result["result"])
874            self.assertEqual(
875                result.get("error", ""),
876                ERROR_MESSAGE.format(101, "get_compatible_elasticsearch_versions"),
877            )
878
879    def test_get_upgrade_history_positive(self):
880        """
881        Test that when calling get_upgrade_history and it
882        succeeds, it returns {'result': True, 'response': some_value}.
883        """
884        ret_val = {
885            "UpgradeHistories": [
886                {
887                    "UpgradeName": "string",
888                    "StartTimestamp": datetime.datetime(2015, 1, 1),
889                    "UpgradeStatus": "IN_PROGRESS",
890                    "StepsList": [
891                        {
892                            "UpgradeStep": "PRE_UPGRADE_CHECK",
893                            "UpgradeStepStatus": "IN_PROGRESS",
894                            "Issues": ["string"],
895                            "ProgressPercent": 123.0,
896                        }
897                    ],
898                }
899            ],
900            "NextToken": "string",
901        }
902        with patch.object(self.paginator, "paginate", return_value=[ret_val]):
903            self.assertEqual(
904                boto3_elasticsearch.get_upgrade_history(
905                    domain_name="testdomain", **CONN_PARAMETERS
906                ),
907                {"result": True, "response": ret_val["UpgradeHistories"]},
908            )
909
910    def test_get_upgrade_history_error(self):
911        """
912        Test that when calling get_upgrade_history and boto3
913        returns an error, it returns {'result': False, 'error': 'the error'}.
914        """
915        with patch.object(
916            self.paginator,
917            "paginate",
918            side_effect=ClientError(ERROR_CONTENT, "get_upgrade_history"),
919        ):
920            result = boto3_elasticsearch.get_upgrade_history(
921                domain_name="testdomain", **CONN_PARAMETERS
922            )
923            self.assertFalse(result["result"])
924            self.assertEqual(
925                result.get("error", ""),
926                ERROR_MESSAGE.format(101, "get_upgrade_history"),
927            )
928
929    def test_get_upgrade_status_positive(self):
930        """
931        Test that when calling get_upgrade_status and it
932        succeeds, it returns {'result': True, 'response': some_value}.
933        """
934        ret_val = {
935            "UpgradeStep": "PRE_UPGRADE_CHECK",
936            "StepStatus": "IN_PROGRESS",
937            "UpgradeName": "string",
938            "ResponseMetadata": None,
939        }
940        with patch.object(self.conn, "get_upgrade_status", return_value=ret_val):
941            self.assertEqual(
942                boto3_elasticsearch.get_upgrade_status(
943                    domain_name="testdomain", **CONN_PARAMETERS
944                ),
945                {"result": True, "response": ret_val},
946            )
947
948    def test_get_upgrade_status_error(self):
949        """
950        Test that when calling get_upgrade_status and boto3
951        returns an error, it returns {'result': False, 'error': 'the error'}.
952        """
953        with patch.object(
954            self.conn,
955            "get_upgrade_status",
956            side_effect=ClientError(ERROR_CONTENT, "get_upgrade_status"),
957        ):
958            result = boto3_elasticsearch.get_upgrade_status(
959                domain_name="testdomain", **CONN_PARAMETERS
960            )
961            self.assertFalse(result["result"])
962            self.assertEqual(
963                result.get("error", ""), ERROR_MESSAGE.format(101, "get_upgrade_status")
964            )
965
966    def test_list_domain_names_positive(self):
967        """
968        Test that when calling list_domain_names and it
969        succeeds, it returns {'result': True, 'response': some_value}.
970        """
971        ret_val = {"DomainNames": [{"DomainName": "string"}]}
972        with patch.object(self.conn, "list_domain_names", return_value=ret_val):
973            self.assertEqual(
974                boto3_elasticsearch.list_domain_names(**CONN_PARAMETERS),
975                {
976                    "result": True,
977                    "response": [item["DomainName"] for item in ret_val["DomainNames"]],
978                },
979            )
980
981    def test_list_domain_names_error(self):
982        """
983        Test that when calling list_domain_names and boto3
984        returns an error, it returns {'result': False, 'error': 'the error'}.
985        """
986        with patch.object(
987            self.conn,
988            "list_domain_names",
989            side_effect=ClientError(ERROR_CONTENT, "list_domain_names"),
990        ):
991            result = boto3_elasticsearch.list_domain_names(**CONN_PARAMETERS)
992            self.assertFalse(result["result"])
993            self.assertEqual(
994                result.get("error", ""), ERROR_MESSAGE.format(101, "list_domain_names")
995            )
996
997    def test_list_elasticsearch_instance_types_positive(self):
998        """
999        Test that when calling list_elasticsearch_instance_types and it
1000        succeeds, it returns {'result': True, 'response': some_value}.
1001        """
1002        ret_val = {
1003            "ElasticsearchInstanceTypes": [
1004                "m3.medium.elasticsearch",
1005                "m3.large.elasticsearch",
1006                "m3.xlarge.elasticsearch",
1007                "m3.2xlarge.elasticsearch",
1008                "m4.large.elasticsearch",
1009                "m4.xlarge.elasticsearch",
1010                "m4.2xlarge.elasticsearch",
1011                "m4.4xlarge.elasticsearch",
1012                "m4.10xlarge.elasticsearch",
1013                "t2.micro.elasticsearch",
1014                "t2.small.elasticsearch",
1015                "t2.medium.elasticsearch",
1016                "r3.large.elasticsearch",
1017                "r3.xlarge.elasticsearch",
1018                "r3.2xlarge.elasticsearch",
1019                "r3.4xlarge.elasticsearch",
1020                "r3.8xlarge.elasticsearch",
1021                "i2.xlarge.elasticsearch",
1022                "i2.2xlarge.elasticsearch",
1023                "d2.xlarge.elasticsearch",
1024                "d2.2xlarge.elasticsearch",
1025                "d2.4xlarge.elasticsearch",
1026                "d2.8xlarge.elasticsearch",
1027                "c4.large.elasticsearch",
1028                "c4.xlarge.elasticsearch",
1029                "c4.2xlarge.elasticsearch",
1030                "c4.4xlarge.elasticsearch",
1031                "c4.8xlarge.elasticsearch",
1032                "r4.large.elasticsearch",
1033                "r4.xlarge.elasticsearch",
1034                "r4.2xlarge.elasticsearch",
1035                "r4.4xlarge.elasticsearch",
1036                "r4.8xlarge.elasticsearch",
1037                "r4.16xlarge.elasticsearch",
1038                "i3.large.elasticsearch",
1039                "i3.xlarge.elasticsearch",
1040                "i3.2xlarge.elasticsearch",
1041                "i3.4xlarge.elasticsearch",
1042                "i3.8xlarge.elasticsearch",
1043                "i3.16xlarge.elasticsearch",
1044            ],
1045            "NextToken": "string",
1046        }
1047        with patch.object(self.paginator, "paginate", return_value=[ret_val]):
1048            self.assertEqual(
1049                boto3_elasticsearch.list_elasticsearch_instance_types(
1050                    elasticsearch_version="1.0", **CONN_PARAMETERS
1051                ),
1052                {"result": True, "response": ret_val["ElasticsearchInstanceTypes"]},
1053            )
1054
1055    def test_list_elasticsearch_instance_types_error(self):
1056        """
1057        Test that when calling list_elasticsearch_instance_types and boto3
1058        returns an error, it returns {'result': False, 'error': 'the error'}.
1059        """
1060        with patch.object(
1061            self.paginator,
1062            "paginate",
1063            side_effect=ClientError(ERROR_CONTENT, "list_elasticsearch_instance_types"),
1064        ):
1065            result = boto3_elasticsearch.list_elasticsearch_instance_types(
1066                elasticsearch_version="1.0", **CONN_PARAMETERS
1067            )
1068            self.assertFalse(result["result"])
1069            self.assertEqual(
1070                result.get("error", ""),
1071                ERROR_MESSAGE.format(101, "list_elasticsearch_instance_types"),
1072            )
1073
1074    def test_list_elasticsearch_versions_positive(self):
1075        """
1076        Test that when calling list_elasticsearch_versions and it
1077        succeeds, it returns {'result': True, 'response': some_value}.
1078        """
1079        ret_val = {"ElasticsearchVersions": ["string"], "NextToken": "string"}
1080        with patch.object(self.paginator, "paginate", return_value=[ret_val]):
1081            self.assertEqual(
1082                boto3_elasticsearch.list_elasticsearch_versions(**CONN_PARAMETERS),
1083                {"result": True, "response": ret_val["ElasticsearchVersions"]},
1084            )
1085
1086    def test_list_elasticsearch_versions_error(self):
1087        """
1088        Test that when calling list_elasticsearch_versions and boto3
1089        returns an error, it returns {'result': False, 'error': 'the error'}.
1090        """
1091        with patch.object(
1092            self.paginator,
1093            "paginate",
1094            side_effect=ClientError(ERROR_CONTENT, "list_elasticsearch_versions"),
1095        ):
1096            result = boto3_elasticsearch.list_elasticsearch_versions(**CONN_PARAMETERS)
1097            self.assertFalse(result["result"])
1098            self.assertEqual(
1099                result.get("error", ""),
1100                ERROR_MESSAGE.format(101, "list_elasticsearch_versions"),
1101            )
1102
1103    def test_purchase_reserved_elasticsearch_instance_offering_positive(self):
1104        """
1105        Test that when calling purchase_reserved_elasticsearch_instance_offering and it
1106        succeeds, it returns {'result': True, 'response': some_value}.
1107        """
1108        ret_val = {
1109            "ReservedElasticsearchInstanceId": "string",
1110            "ReservationName": "string",
1111        }
1112        with patch.object(
1113            self.conn,
1114            "purchase_reserved_elasticsearch_instance_offering",
1115            return_value=ret_val,
1116        ):
1117            self.assertEqual(
1118                boto3_elasticsearch.purchase_reserved_elasticsearch_instance_offering(
1119                    reserved_elasticsearch_instance_offering_id="foo",
1120                    reservation_name="bar",
1121                    **CONN_PARAMETERS
1122                ),
1123                {"result": True, "response": ret_val},
1124            )
1125
1126    def test_purchase_reserved_elasticsearch_instance_offering_error(self):
1127        """
1128        Test that when calling purchase_reserved_elasticsearch_instance_offering and boto3
1129        returns an error, it returns {'result': False, 'error': 'the error'}.
1130        """
1131        with patch.object(
1132            self.conn,
1133            "purchase_reserved_elasticsearch_instance_offering",
1134            side_effect=ClientError(
1135                ERROR_CONTENT, "purchase_reserved_elasticsearch_instance_offering"
1136            ),
1137        ):
1138            result = (
1139                boto3_elasticsearch.purchase_reserved_elasticsearch_instance_offering(
1140                    reserved_elasticsearch_instance_offering_id="foo",
1141                    reservation_name="bar",
1142                    **CONN_PARAMETERS
1143                )
1144            )
1145            self.assertFalse(result["result"])
1146            self.assertEqual(
1147                result.get("error", ""),
1148                ERROR_MESSAGE.format(
1149                    101, "purchase_reserved_elasticsearch_instance_offering"
1150                ),
1151            )
1152
1153    def test_start_elasticsearch_service_software_update_positive(self):
1154        """
1155        Test that when calling start_elasticsearch_service_software_update and it
1156        succeeds, it returns {'result': True, 'response': some_value}.
1157        """
1158        ret_val = {
1159            "ServiceSoftwareOptions": {
1160                "CurrentVersion": "string",
1161                "NewVersion": "string",
1162                "UpdateAvailable": True,
1163                "Cancellable": True,
1164                "UpdateStatus": "PENDING_UPDATE",
1165                "Description": "string",
1166                "AutomatedUpdateDate": datetime.datetime(2015, 1, 1),
1167            }
1168        }
1169        with patch.object(
1170            self.conn,
1171            "start_elasticsearch_service_software_update",
1172            return_value=ret_val,
1173        ):
1174            self.assertEqual(
1175                boto3_elasticsearch.start_elasticsearch_service_software_update(
1176                    domain_name="testdomain", **CONN_PARAMETERS
1177                ),
1178                {"result": True, "response": ret_val["ServiceSoftwareOptions"]},
1179            )
1180
1181    def test_start_elasticsearch_service_software_update_error(self):
1182        """
1183        Test that when calling start_elasticsearch_service_software_update and boto3
1184        returns an error, it returns {'result': False, 'error': 'the error'}.
1185        """
1186        with patch.object(
1187            self.conn,
1188            "start_elasticsearch_service_software_update",
1189            side_effect=ClientError(
1190                ERROR_CONTENT, "start_elasticsearch_service_software_update"
1191            ),
1192        ):
1193            result = boto3_elasticsearch.start_elasticsearch_service_software_update(
1194                domain_name="testdomain", **CONN_PARAMETERS
1195            )
1196            self.assertFalse(result["result"])
1197            self.assertEqual(
1198                result.get("error", ""),
1199                ERROR_MESSAGE.format(
1200                    101, "start_elasticsearch_service_software_update"
1201                ),
1202            )
1203
1204    def test_upgrade_elasticsearch_domain_positive(self):
1205        """
1206        Test that when calling upgrade_elasticsearch_domain and it
1207        succeeds, it returns {'result': True, 'response': some_value}.
1208        """
1209        ret_val = {
1210            "DomainName": "string",
1211            "TargetVersion": "string",
1212            "PerformCheckOnly": True,
1213        }
1214        with patch.object(
1215            self.conn, "upgrade_elasticsearch_domain", return_value=ret_val
1216        ):
1217            self.assertEqual(
1218                boto3_elasticsearch.upgrade_elasticsearch_domain(
1219                    domain_name="testdomain", target_version="1.1", **CONN_PARAMETERS
1220                ),
1221                {"result": True, "response": ret_val},
1222            )
1223
1224    def test_upgrade_elasticsearch_domain_error(self):
1225        """
1226        Test that when calling upgrade_elasticsearch_domain and boto3
1227        returns an error, it returns {'result': False, 'error': 'the error'}.
1228        """
1229        with patch.object(
1230            self.conn,
1231            "upgrade_elasticsearch_domain",
1232            side_effect=ClientError(ERROR_CONTENT, "upgrade_elasticsearch_domain"),
1233        ):
1234            result = boto3_elasticsearch.upgrade_elasticsearch_domain(
1235                domain_name="testdomain", target_version="1.1", **CONN_PARAMETERS
1236            )
1237            self.assertFalse(result["result"])
1238            self.assertEqual(
1239                result.get("error", ""),
1240                ERROR_MESSAGE.format(101, "upgrade_elasticsearch_domain"),
1241            )
1242