diff --git a/lib/ansible/modules/notification/slack.py b/lib/ansible/modules/notification/slack.py index 9c7c2f160c0..7bf4026540e 100644 --- a/lib/ansible/modules/notification/slack.py +++ b/lib/ansible/modules/notification/slack.py @@ -55,6 +55,10 @@ options: channel: description: - Channel to send the message to. If absent, the message goes to the channel selected for the I(token). + thread_id: + version_added: 2.8 + description: + - Optional. Timestamp of message to thread this message to as a float. https://api.slack.com/docs/message-threading username: description: - This is the sender of the message. @@ -113,6 +117,7 @@ EXAMPLES = """ token: thetoken/generatedby/slack msg: '{{ inventory_hostname }} completed' channel: '#ansible' + thread_id: 1539917263.000100 username: 'Ansible on {{ inventory_hostname }}' icon_url: http://www.example.com/some-image-file.png link_names: 0 @@ -173,7 +178,8 @@ def escape_quotes(text): return "".join(escape_table.get(c, c) for c in text) -def build_payload_for_slack(module, text, channel, username, icon_url, icon_emoji, link_names, parse, color, attachments): +def build_payload_for_slack(module, text, channel, thread_id, username, icon_url, icon_emoji, link_names, + parse, color, attachments): payload = {} if color == "normal" and text is not None: payload = dict(text=escape_quotes(text)) @@ -185,6 +191,8 @@ def build_payload_for_slack(module, text, channel, username, icon_url, icon_emoj payload['channel'] = channel else: payload['channel'] = '#' + channel + if thread_id is not None: + payload['thread_ts'] = thread_id if username is not None: payload['username'] = username if icon_emoji is not None: @@ -228,7 +236,8 @@ def do_notify_slack(module, domain, token, payload): slack_incoming_webhook = SLACK_INCOMING_WEBHOOK % (token) else: if not domain: - module.fail_json(msg="Slack has updated its webhook API. You need to specify a token of the form XXXX/YYYY/ZZZZ in your playbook") + module.fail_json(msg="Slack has updated its webhook API. You need to specify a token of the form " + "XXXX/YYYY/ZZZZ in your playbook") slack_incoming_webhook = OLD_SLACK_INCOMING_WEBHOOK % (domain, token) headers = { @@ -249,6 +258,7 @@ def main(): token=dict(type='str', required=True, no_log=True), msg=dict(type='str', required=False, default=None), channel=dict(type='str', default=None), + thread_id=dict(type='float', default=None), username=dict(type='str', default='Ansible'), icon_url=dict(type='str', default='https://www.ansible.com/favicon.ico'), icon_emoji=dict(type='str', default=None), @@ -264,6 +274,7 @@ def main(): token = module.params['token'] text = module.params['msg'] channel = module.params['channel'] + thread_id = module.params['thread_id'] username = module.params['username'] icon_url = module.params['icon_url'] icon_emoji = module.params['icon_emoji'] @@ -272,7 +283,8 @@ def main(): color = module.params['color'] attachments = module.params['attachments'] - payload = build_payload_for_slack(module, text, channel, username, icon_url, icon_emoji, link_names, parse, color, attachments) + payload = build_payload_for_slack(module, text, channel, thread_id, username, icon_url, icon_emoji, link_names, + parse, color, attachments) do_notify_slack(module, domain, token, payload) module.exit_json(msg="OK") diff --git a/test/units/modules/notification/test_slack.py b/test/units/modules/notification/test_slack.py new file mode 100644 index 00000000000..5b325cbeb59 --- /dev/null +++ b/test/units/modules/notification/test_slack.py @@ -0,0 +1,85 @@ +import json +import pytest +from units.compat.mock import patch +from ansible.modules.notification import slack +from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args +from ansible import module_utils + + +class TestSlackModule(ModuleTestCase): + + def setUp(self): + super(TestSlackModule, self).setUp() + self.module = slack + + def tearDown(self): + super(TestSlackModule, self).tearDown() + + @pytest.fixture + def fetch_url_mock(self, mocker): + return mocker.patch('ansible.module_utils.notification.slack.fetch_url') + + def test_without_required_parameters(self): + """Failure must occurs when all parameters are missing""" + with self.assertRaises(AnsibleFailJson): + set_module_args({}) + self.module.main() + + def test_invalid_old_token(self): + """Failure if there is an old style token""" + set_module_args({ + 'token': 'test', + }) + with self.assertRaises(AnsibleFailJson): + self.module.main() + + def test_sucessful_message(self): + """tests sending a message. This is example 1 from the docs""" + set_module_args({ + 'token': 'XXXX/YYYY/ZZZZ', + 'msg': 'test' + }) + + with patch.object(slack, "fetch_url") as fetch_url_mock: + fetch_url_mock.return_value = (None, {"status": 200}) + with self.assertRaises(AnsibleExitJson): + self.module.main() + + self.assertTrue(fetch_url_mock.call_count, 1) + call_data = json.loads(fetch_url_mock.call_args[1]['data']) + assert call_data['username'] == "Ansible" + assert call_data['text'] == "test" + assert fetch_url_mock.call_args[1]['url'] == "https://hooks.slack.com/services/XXXX/YYYY/ZZZZ" + + def test_failed_message(self): + """tests failing to send a message""" + + set_module_args({ + 'token': 'XXXX/YYYY/ZZZZ', + 'msg': 'test' + }) + + with patch.object(slack, "fetch_url") as fetch_url_mock: + fetch_url_mock.return_value = (None, {"status": 404, 'msg': 'test'}) + with self.assertRaises(AnsibleFailJson): + self.module.main() + + def test_message_with_thread(self): + """tests sending a message with a thread""" + set_module_args({ + 'token': 'XXXX/YYYY/ZZZZ', + 'msg': 'test', + 'thread_id': 100.00 + }) + + with patch.object(slack, "fetch_url") as fetch_url_mock: + fetch_url_mock.return_value = (None, {"status": 200}) + with self.assertRaises(AnsibleExitJson): + self.module.main() + + self.assertTrue(fetch_url_mock.call_count, 1) + call_data = json.loads(fetch_url_mock.call_args[1]['data']) + assert call_data['username'] == "Ansible" + assert call_data['text'] == "test" + assert call_data['thread_ts'] == 100.00 + assert fetch_url_mock.call_args[1]['url'] == "https://hooks.slack.com/services/XXXX/YYYY/ZZZZ"