test_boto_iot.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. # -*- coding: utf-8 -*-
  2. # Import Python libs
  3. from __future__ import absolute_import, print_function, unicode_literals
  4. import logging
  5. import random
  6. import string
  7. import pytest
  8. # Import Salt libs
  9. import salt.config
  10. import salt.loader
  11. import salt.states.boto_iot as boto_iot
  12. # Import 3rd-party libs
  13. from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin
  14. from salt.utils.versions import LooseVersion
  15. # Import Salt Testing libs
  16. from tests.support.mixins import LoaderModuleMockMixin
  17. from tests.support.mock import MagicMock, patch
  18. from tests.support.unit import TestCase, skipIf
  19. # pylint: disable=import-error,no-name-in-module,unused-import
  20. from tests.unit.modules.test_boto_iot import BotoIoTTestCaseMixin
  21. # Import test suite libs
  22. try:
  23. import boto
  24. import boto3
  25. from botocore.exceptions import ClientError
  26. from botocore import __version__ as found_botocore_version
  27. HAS_BOTO = True
  28. except ImportError:
  29. HAS_BOTO = False
  30. # pylint: enable=import-error,no-name-in-module,unused-import
  31. # the boto_iot module relies on the connect_to_region() method
  32. # which was added in boto 2.8.0
  33. # https://github.com/boto/boto/commit/33ac26b416fbb48a60602542b4ce15dcc7029f12
  34. required_boto3_version = "1.2.1"
  35. required_botocore_version = "1.4.41"
  36. log = logging.getLogger(__name__)
  37. def _has_required_boto():
  38. """
  39. Returns True/False boolean depending on if Boto is installed and correct
  40. version.
  41. """
  42. if not HAS_BOTO:
  43. return False
  44. elif LooseVersion(boto3.__version__) < LooseVersion(required_boto3_version):
  45. return False
  46. elif LooseVersion(found_botocore_version) < LooseVersion(required_botocore_version):
  47. return False
  48. else:
  49. return True
  50. if _has_required_boto():
  51. region = "us-east-1"
  52. access_key = "GKTADJGHEIQSXMKKRBJ08H"
  53. secret_key = "askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs"
  54. conn_parameters = {
  55. "region": region,
  56. "key": access_key,
  57. "keyid": secret_key,
  58. "profile": {},
  59. }
  60. error_message = (
  61. "An error occurred (101) when calling the {0} operation: Test-defined error"
  62. )
  63. not_found_error = ClientError(
  64. {
  65. "Error": {
  66. "Code": "ResourceNotFoundException",
  67. "Message": "Test-defined error",
  68. }
  69. },
  70. "msg",
  71. )
  72. topic_rule_not_found_error = ClientError(
  73. {"Error": {"Code": "UnauthorizedException", "Message": "Test-defined error"}},
  74. "msg",
  75. )
  76. error_content = {"Error": {"Code": 101, "Message": "Test-defined error"}}
  77. policy_ret = dict(
  78. policyName="testpolicy",
  79. policyDocument='{"Version": "2012-10-17", "Statement": [{"Action": ["iot:Publish"], "Resource": ["*"], "Effect": "Allow"}]}',
  80. policyArn="arn:aws:iot:us-east-1:123456:policy/my_policy",
  81. policyVersionId=1,
  82. defaultVersionId=1,
  83. )
  84. topic_rule_ret = dict(
  85. ruleName="testrule",
  86. sql="SELECT * FROM 'iot/test'",
  87. description="topic rule description",
  88. createdAt="1970-01-01",
  89. actions=[{"iot": {"functionArn": "arn:aws:::function"}}],
  90. ruleDisabled=True,
  91. )
  92. principal = "arn:aws:iot:us-east-1:1234:cert/21fc104aaaf6043f5756c1b57bda84ea8395904c43f28517799b19e4c42514"
  93. thing_type_name = "test_thing_type"
  94. thing_type_desc = "test_thing_type_desc"
  95. thing_type_attr_1 = "test_thing_type_search_attr_1"
  96. thing_type_ret = dict(
  97. thingTypeName=thing_type_name,
  98. thingTypeProperties=dict(
  99. thingTypeDescription=thing_type_desc,
  100. searchableAttributes=[thing_type_attr_1],
  101. ),
  102. thingTypeMetadata=dict(
  103. deprecated=False, creationDate="2010-08-01 15:54:49.699000+00:00"
  104. ),
  105. )
  106. deprecated_thing_type_ret = dict(
  107. thingTypeName=thing_type_name,
  108. thingTypeProperties=dict(
  109. thingTypeDescription=thing_type_desc,
  110. searchableAttributes=[thing_type_attr_1],
  111. ),
  112. thingTypeMetadata=dict(
  113. deprecated=True,
  114. creationDate="2010-08-01 15:54:49.699000+00:00",
  115. deprecationDate="2010-08-02 15:54:49.699000+00:00",
  116. ),
  117. )
  118. thing_type_arn = "test_thing_type_arn"
  119. create_thing_type_ret = dict(
  120. thingTypeName=thing_type_name, thingTypeArn=thing_type_arn
  121. )
  122. @skipIf(HAS_BOTO is False, "The boto module must be installed.")
  123. @skipIf(
  124. _has_required_boto() is False,
  125. "The boto3 module must be greater than"
  126. " or equal to version {0}".format(required_boto3_version),
  127. )
  128. class BotoIoTStateTestCaseBase(TestCase, LoaderModuleMockMixin):
  129. conn = None
  130. def setup_loader_modules(self):
  131. ctx = {}
  132. utils = salt.loader.utils(
  133. self.opts,
  134. whitelist=["boto3", "args", "systemd", "path", "platform"],
  135. context=ctx,
  136. )
  137. serializers = salt.loader.serializers(self.opts)
  138. self.funcs = funcs = salt.loader.minion_mods(
  139. self.opts, context=ctx, utils=utils, whitelist=["boto_iot"]
  140. )
  141. self.salt_states = salt.loader.states(
  142. opts=self.opts,
  143. functions=funcs,
  144. utils=utils,
  145. whitelist=["boto_iot"],
  146. serializers=serializers,
  147. )
  148. return {
  149. boto_iot: {
  150. "__opts__": self.opts,
  151. "__salt__": funcs,
  152. "__utils__": utils,
  153. "__states__": self.salt_states,
  154. "__serializers__": serializers,
  155. }
  156. }
  157. @classmethod
  158. def setUpClass(cls):
  159. cls.opts = salt.config.DEFAULT_MINION_OPTS.copy()
  160. cls.opts["grains"] = salt.loader.grains(cls.opts)
  161. @classmethod
  162. def tearDownClass(cls):
  163. del cls.opts
  164. def setUp(self):
  165. self.addCleanup(delattr, self, "funcs")
  166. self.addCleanup(delattr, self, "salt_states")
  167. # Set up MagicMock to replace the boto3 session
  168. # connections keep getting cached from prior tests, can't find the
  169. # correct context object to clear it. So randomize the cache key, to prevent any
  170. # cache hits
  171. conn_parameters["key"] = "".join(
  172. random.choice(string.ascii_lowercase + string.digits) for _ in range(50)
  173. )
  174. self.patcher = patch("boto3.session.Session")
  175. self.addCleanup(self.patcher.stop)
  176. self.addCleanup(delattr, self, "patcher")
  177. mock_session = self.patcher.start()
  178. session_instance = mock_session.return_value
  179. self.conn = MagicMock()
  180. self.addCleanup(delattr, self, "conn")
  181. session_instance.client.return_value = self.conn
  182. class BotoIoTThingTypeTestCase(BotoIoTStateTestCaseBase, BotoIoTTestCaseMixin):
  183. """
  184. TestCase for salt.modules.boto_iot state.module
  185. """
  186. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  187. def test_present_when_thing_type_does_not_exist(self):
  188. """
  189. tests present on a thing type that does not exist.
  190. """
  191. self.conn.describe_thing_type.side_effect = [not_found_error, thing_type_ret]
  192. self.conn.create_thing_type.return_value = create_thing_type_ret
  193. result = self.salt_states["boto_iot.thing_type_present"](
  194. "thing type present",
  195. thingTypeName=thing_type_name,
  196. thingTypeDescription=thing_type_desc,
  197. searchableAttributesList=[thing_type_attr_1],
  198. **conn_parameters
  199. )
  200. self.assertTrue(result["result"])
  201. self.assertEqual(
  202. result["changes"]["new"]["thing_type"]["thingTypeName"], thing_type_name
  203. )
  204. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  205. def test_present_when_thing_type_exists(self):
  206. self.conn.describe_thing_type.return_value = thing_type_ret
  207. result = self.salt_states["boto_iot.thing_type_present"](
  208. "thing type present",
  209. thingTypeName=thing_type_name,
  210. thingTypeDescription=thing_type_desc,
  211. searchableAttributesList=[thing_type_attr_1],
  212. **conn_parameters
  213. )
  214. self.assertTrue(result["result"])
  215. self.assertEqual(result["changes"], {})
  216. self.assertTrue(self.conn.create_thing_type.call_count == 0)
  217. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  218. def test_present_with_failure(self):
  219. self.conn.describe_thing_type.side_effect = [not_found_error, thing_type_ret]
  220. self.conn.create_thing_type.side_effect = ClientError(
  221. error_content, "create_thing_type"
  222. )
  223. result = self.salt_states["boto_iot.thing_type_present"](
  224. "thing type present",
  225. thingTypeName=thing_type_name,
  226. thingTypeDescription=thing_type_desc,
  227. searchableAttributesList=[thing_type_attr_1],
  228. **conn_parameters
  229. )
  230. self.assertFalse(result["result"])
  231. self.assertTrue("An error occurred" in result["comment"])
  232. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  233. def test_absent_when_thing_type_does_not_exist(self):
  234. """
  235. Tests absent on a thing type does not exist
  236. """
  237. self.conn.describe_thing_type.side_effect = not_found_error
  238. result = self.salt_states["boto_iot.thing_type_absent"](
  239. "test", "mythingtype", **conn_parameters
  240. )
  241. self.assertTrue(result["result"])
  242. self.assertEqual(result["changes"], {})
  243. @pytest.mark.slow_test(seconds=120) # Test takes >60 and <=120 seconds
  244. def test_absent_when_thing_type_exists(self):
  245. """
  246. Tests absent on a thing type
  247. """
  248. self.conn.describe_thing_type.return_value = deprecated_thing_type_ret
  249. result = self.salt_states["boto_iot.thing_type_absent"](
  250. "test", thing_type_name, **conn_parameters
  251. )
  252. self.assertTrue(result["result"])
  253. self.assertEqual(result["changes"]["new"]["thing_type"], None)
  254. self.assertTrue(self.conn.deprecate_thing_type.call_count == 0)
  255. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  256. def test_absent_with_deprecate_failure(self):
  257. self.conn.describe_thing_type.return_value = thing_type_ret
  258. self.conn.deprecate_thing_type.side_effect = ClientError(
  259. error_content, "deprecate_thing_type"
  260. )
  261. result = self.salt_states["boto_iot.thing_type_absent"](
  262. "test", thing_type_name, **conn_parameters
  263. )
  264. self.assertFalse(result["result"])
  265. self.assertTrue("An error occurred" in result["comment"])
  266. self.assertTrue("deprecate_thing_type" in result["comment"])
  267. self.assertTrue(self.conn.delete_thing_type.call_count == 0)
  268. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  269. def test_absent_with_delete_failure(self):
  270. self.conn.describe_thing_type.return_value = deprecated_thing_type_ret
  271. self.conn.delete_thing_type.side_effect = ClientError(
  272. error_content, "delete_thing_type"
  273. )
  274. result = self.salt_states["boto_iot.thing_type_absent"](
  275. "test", thing_type_name, **conn_parameters
  276. )
  277. self.assertFalse(result["result"])
  278. self.assertTrue("An error occurred" in result["comment"])
  279. self.assertTrue("delete_thing_type" in result["comment"])
  280. self.assertTrue(self.conn.deprecate_thing_type.call_count == 0)
  281. class BotoIoTPolicyTestCase(BotoIoTStateTestCaseBase, BotoIoTTestCaseMixin):
  282. """
  283. TestCase for salt.modules.boto_iot state.module
  284. """
  285. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  286. def test_present_when_policy_does_not_exist(self):
  287. """
  288. Tests present on a policy that does not exist.
  289. """
  290. self.conn.get_policy.side_effect = [not_found_error, policy_ret]
  291. self.conn.create_policy.return_value = policy_ret
  292. result = self.salt_states["boto_iot.policy_present"](
  293. "policy present",
  294. policyName=policy_ret["policyName"],
  295. policyDocument=policy_ret["policyDocument"],
  296. )
  297. self.assertTrue(result["result"])
  298. self.assertEqual(
  299. result["changes"]["new"]["policy"]["policyName"], policy_ret["policyName"]
  300. )
  301. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  302. def test_present_when_policy_exists(self):
  303. self.conn.get_policy.return_value = policy_ret
  304. self.conn.create_policy_version.return_value = policy_ret
  305. result = self.salt_states["boto_iot.policy_present"](
  306. "policy present",
  307. policyName=policy_ret["policyName"],
  308. policyDocument=policy_ret["policyDocument"],
  309. )
  310. self.assertTrue(result["result"])
  311. self.assertEqual(result["changes"], {})
  312. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  313. def test_present_with_failure(self):
  314. self.conn.get_policy.side_effect = [not_found_error, policy_ret]
  315. self.conn.create_policy.side_effect = ClientError(
  316. error_content, "create_policy"
  317. )
  318. result = self.salt_states["boto_iot.policy_present"](
  319. "policy present",
  320. policyName=policy_ret["policyName"],
  321. policyDocument=policy_ret["policyDocument"],
  322. )
  323. self.assertFalse(result["result"])
  324. self.assertTrue("An error occurred" in result["comment"])
  325. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  326. def test_absent_when_policy_does_not_exist(self):
  327. """
  328. Tests absent on a policy that does not exist.
  329. """
  330. self.conn.get_policy.side_effect = not_found_error
  331. result = self.salt_states["boto_iot.policy_absent"]("test", "mypolicy")
  332. self.assertTrue(result["result"])
  333. self.assertEqual(result["changes"], {})
  334. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  335. def test_absent_when_policy_exists(self):
  336. self.conn.get_policy.return_value = policy_ret
  337. self.conn.list_policy_versions.return_value = {"policyVersions": []}
  338. result = self.salt_states["boto_iot.policy_absent"](
  339. "test", policy_ret["policyName"]
  340. )
  341. self.assertTrue(result["result"])
  342. self.assertEqual(result["changes"]["new"]["policy"], None)
  343. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  344. def test_absent_with_failure(self):
  345. self.conn.get_policy.return_value = policy_ret
  346. self.conn.list_policy_versions.return_value = {"policyVersions": []}
  347. self.conn.delete_policy.side_effect = ClientError(
  348. error_content, "delete_policy"
  349. )
  350. result = self.salt_states["boto_iot.policy_absent"](
  351. "test", policy_ret["policyName"]
  352. )
  353. self.assertFalse(result["result"])
  354. self.assertTrue("An error occurred" in result["comment"])
  355. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  356. def test_attached_when_policy_not_attached(self):
  357. """
  358. Tests attached on a policy that is not attached.
  359. """
  360. self.conn.list_principal_policies.return_value = {"policies": []}
  361. result = self.salt_states["boto_iot.policy_attached"](
  362. "test", "myfunc", principal
  363. )
  364. self.assertTrue(result["result"])
  365. self.assertTrue(result["changes"]["new"]["attached"])
  366. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  367. def test_attached_when_policy_attached(self):
  368. """
  369. Tests attached on a policy that is attached.
  370. """
  371. self.conn.list_principal_policies.return_value = {"policies": [policy_ret]}
  372. result = self.salt_states["boto_iot.policy_attached"](
  373. "test", policy_ret["policyName"], principal
  374. )
  375. self.assertTrue(result["result"])
  376. self.assertEqual(result["changes"], {})
  377. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  378. def test_attached_with_failure(self):
  379. """
  380. Tests attached on a policy that is attached.
  381. """
  382. self.conn.list_principal_policies.return_value = {"policies": []}
  383. self.conn.attach_principal_policy.side_effect = ClientError(
  384. error_content, "attach_principal_policy"
  385. )
  386. result = self.salt_states["boto_iot.policy_attached"](
  387. "test", policy_ret["policyName"], principal
  388. )
  389. self.assertFalse(result["result"])
  390. self.assertEqual(result["changes"], {})
  391. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  392. def test_detached_when_policy_not_detached(self):
  393. """
  394. Tests detached on a policy that is not detached.
  395. """
  396. self.conn.list_principal_policies.return_value = {"policies": [policy_ret]}
  397. result = self.salt_states["boto_iot.policy_detached"](
  398. "test", policy_ret["policyName"], principal
  399. )
  400. self.assertTrue(result["result"])
  401. log.warning(result)
  402. self.assertFalse(result["changes"]["new"]["attached"])
  403. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  404. def test_detached_when_policy_detached(self):
  405. """
  406. Tests detached on a policy that is detached.
  407. """
  408. self.conn.list_principal_policies.return_value = {"policies": []}
  409. result = self.salt_states["boto_iot.policy_detached"](
  410. "test", policy_ret["policyName"], principal
  411. )
  412. self.assertTrue(result["result"])
  413. self.assertEqual(result["changes"], {})
  414. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  415. def test_detached_with_failure(self):
  416. """
  417. Tests detached on a policy that is detached.
  418. """
  419. self.conn.list_principal_policies.return_value = {"policies": [policy_ret]}
  420. self.conn.detach_principal_policy.side_effect = ClientError(
  421. error_content, "detach_principal_policy"
  422. )
  423. result = self.salt_states["boto_iot.policy_detached"](
  424. "test", policy_ret["policyName"], principal
  425. )
  426. self.assertFalse(result["result"])
  427. self.assertEqual(result["changes"], {})
  428. @skipIf(HAS_BOTO is False, "The boto module must be installed.")
  429. @skipIf(
  430. _has_required_boto() is False,
  431. "The boto3 module must be greater than"
  432. " or equal to version {0}. The botocore"
  433. " module must be greater than or equal to"
  434. " version {1}.".format(required_boto3_version, required_botocore_version),
  435. )
  436. class BotoIoTTopicRuleTestCase(BotoIoTStateTestCaseBase, BotoIoTTestCaseMixin):
  437. """
  438. TestCase for salt.modules.boto_iot state.module rules
  439. """
  440. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  441. def test_present_when_topic_rule_does_not_exist(self):
  442. """
  443. Tests present on a topic_rule that does not exist.
  444. """
  445. self.conn.get_topic_rule.side_effect = [
  446. topic_rule_not_found_error,
  447. {"rule": topic_rule_ret},
  448. ]
  449. self.conn.create_topic_rule.return_value = {"created": True}
  450. result = self.salt_states["boto_iot.topic_rule_present"](
  451. "topic rule present",
  452. ruleName=topic_rule_ret["ruleName"],
  453. sql=topic_rule_ret["sql"],
  454. description=topic_rule_ret["description"],
  455. actions=topic_rule_ret["actions"],
  456. ruleDisabled=topic_rule_ret["ruleDisabled"],
  457. )
  458. self.assertTrue(result["result"])
  459. self.assertEqual(
  460. result["changes"]["new"]["rule"]["ruleName"], topic_rule_ret["ruleName"]
  461. )
  462. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  463. def test_present_when_policy_exists(self):
  464. self.conn.get_topic_rule.return_value = {"rule": topic_rule_ret}
  465. self.conn.create_topic_rule.return_value = {"created": True}
  466. result = self.salt_states["boto_iot.topic_rule_present"](
  467. "topic rule present",
  468. ruleName=topic_rule_ret["ruleName"],
  469. sql=topic_rule_ret["sql"],
  470. description=topic_rule_ret["description"],
  471. actions=topic_rule_ret["actions"],
  472. ruleDisabled=topic_rule_ret["ruleDisabled"],
  473. )
  474. self.assertTrue(result["result"])
  475. self.assertEqual(result["changes"], {})
  476. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  477. def test_present_with_failure(self):
  478. self.conn.get_topic_rule.side_effect = [
  479. topic_rule_not_found_error,
  480. {"rule": topic_rule_ret},
  481. ]
  482. self.conn.create_topic_rule.side_effect = ClientError(
  483. error_content, "create_topic_rule"
  484. )
  485. result = self.salt_states["boto_iot.topic_rule_present"](
  486. "topic rule present",
  487. ruleName=topic_rule_ret["ruleName"],
  488. sql=topic_rule_ret["sql"],
  489. description=topic_rule_ret["description"],
  490. actions=topic_rule_ret["actions"],
  491. ruleDisabled=topic_rule_ret["ruleDisabled"],
  492. )
  493. self.assertFalse(result["result"])
  494. self.assertTrue("An error occurred" in result["comment"])
  495. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  496. def test_absent_when_topic_rule_does_not_exist(self):
  497. """
  498. Tests absent on a topic rule that does not exist.
  499. """
  500. self.conn.get_topic_rule.side_effect = topic_rule_not_found_error
  501. result = self.salt_states["boto_iot.topic_rule_absent"]("test", "myrule")
  502. self.assertTrue(result["result"])
  503. self.assertEqual(result["changes"], {})
  504. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  505. def test_absent_when_topic_rule_exists(self):
  506. self.conn.get_topic_rule.return_value = topic_rule_ret
  507. result = self.salt_states["boto_iot.topic_rule_absent"](
  508. "test", topic_rule_ret["ruleName"]
  509. )
  510. self.assertTrue(result["result"])
  511. self.assertEqual(result["changes"]["new"]["rule"], None)
  512. @pytest.mark.slow_test(seconds=1) # Test takes >0.1 and <=1 seconds
  513. def test_absent_with_failure(self):
  514. self.conn.get_topic_rule.return_value = topic_rule_ret
  515. self.conn.delete_topic_rule.side_effect = ClientError(
  516. error_content, "delete_topic_rule"
  517. )
  518. result = self.salt_states["boto_iot.topic_rule_absent"](
  519. "test", topic_rule_ret["ruleName"]
  520. )
  521. self.assertFalse(result["result"])
  522. self.assertTrue("An error occurred" in result["comment"])