From 209bc08eaf4cc22e50d7a8ab7b43edef2a438551 Mon Sep 17 00:00:00 2001 From: Tom Scholz <> Date: Fri, 5 Jun 2026 09:25:23 +0200 Subject: [PATCH] refactor: split webhook_url into separate module parameters --- plugins/modules/google_chat.py | 62 ++++++++++++++----- .../unit/plugins/modules/test_google_chat.py | 49 +++++++++------ 2 files changed, 76 insertions(+), 35 deletions(-) diff --git a/plugins/modules/google_chat.py b/plugins/modules/google_chat.py index fd7d27a04f..d120662425 100644 --- a/plugins/modules/google_chat.py +++ b/plugins/modules/google_chat.py @@ -23,11 +23,24 @@ attributes: diff_mode: support: none options: - webhook_url: + space: type: str required: true description: - - The incoming webhook URL for the Chat space, including the C(key) and C(token) query parameters. + - The identifier of the Chat space to post to, taken from the incoming webhook URL. + - For a webhook URL of the form C(https://chat.googleapis.com/v1/spaces/AAAA/messages?key=...&token=...), + this is the C(AAAA) part. + key: + type: str + required: true + description: + - The C(key) request parameter from the incoming webhook URL. + - Keep this value secret as it grants the ability to post to the space. + token: + type: str + required: true + description: + - The C(token) request parameter from the incoming webhook URL. - Keep this value secret as it grants the ability to post to the space. text: type: str @@ -64,13 +77,17 @@ seealso: EXAMPLES = r""" - name: Send a notification to Google Chat community.general.google_chat: - webhook_url: "https://chat.googleapis.com/v1/spaces/SPACE_ID/messages?key=KEY&token=TOKEN" + space: SPACE_ID + key: KEY + token: TOKEN text: '{{ inventory_hostname }} completed' delegate_to: localhost - name: Start a thread community.general.google_chat: - webhook_url: "https://chat.googleapis.com/v1/spaces/SPACE_ID/messages?key=KEY&token=TOKEN" + space: SPACE_ID + key: KEY + token: TOKEN text: 'Starting a thread' thread_key: 'deploy-2026-06-01' create_new_thread: true @@ -81,7 +98,9 @@ EXAMPLES = r""" # Note: webhooks are rate-limited to 1 request per second per space. - name: Announce deploy start (starts the thread) community.general.google_chat: - webhook_url: "{{ chat_webhook }}" + space: "{{ chat_space }}" + key: "{{ chat_key }}" + token: "{{ chat_token }}" text: "🚀 Starting deploy of *{{ app_version | default('latest') }}* to {{ inventory_hostname }}" thread_key: "{{ deploy_thread }}" create_new_thread: true @@ -92,8 +111,10 @@ EXAMPLES = r""" - name: Report a step into the same thread community.general.google_chat: - webhook_url: "{{ chat_webhook }}" - text: "✅ Step 1/3 – code checked out" + space: "{{ chat_space }}" + key: "{{ chat_key }}" + token: "{{ chat_token }}" + text: "✅ Step 1/3 — code checked out" thread_key: "{{ deploy_thread }}" create_new_thread: false delegate_to: localhost @@ -109,7 +130,9 @@ EXAMPLES = r""" - name: Report success community.general.google_chat: - webhook_url: "{{ chat_webhook }}" + space: "{{ chat_space }}" + key: "{{ chat_key }}" + token: "{{ chat_token }}" text: "🎉 Deploy to {{ inventory_hostname }} complete" thread_key: "{{ deploy_thread }}" create_new_thread: false @@ -118,8 +141,10 @@ EXAMPLES = r""" rescue: - name: Report failure into the thread community.general.google_chat: - webhook_url: "{{ chat_webhook }}" - text: "❌ Deploy to {{ inventory_hostname }} *failed* – {{ ansible_failed_task.name }}" + space: "{{ chat_space }}" + key: "{{ chat_key }}" + token: "{{ chat_token }}" + text: "❌ Deploy to {{ inventory_hostname }} *failed* — {{ ansible_failed_task.name }}" thread_key: "{{ deploy_thread }}" create_new_thread: false delegate_to: localhost @@ -148,6 +173,8 @@ from urllib.parse import urlencode from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.urls import fetch_url +BASE_URL = "https://chat.googleapis.com/v1/spaces" + def build_payload(text, thread_key): payload = {"text": text} @@ -156,14 +183,13 @@ def build_payload(text, thread_key): return payload -def build_url(webhook_url, thread_key, create_new_thread): - params = {} +def build_url(space, key, token, thread_key, create_new_thread): + params = {"key": key, "token": token} if thread_key is not None: params["messageReplyOption"] = ( "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD" if create_new_thread else "REPLY_MESSAGE_OR_FAIL" ) - sep = "&" if "?" in webhook_url else "?" - return f"{webhook_url}{sep}{urlencode(params)}" + return f"{BASE_URL}/{space}/messages?{urlencode(params)}" def do_notify(module, url, payload): @@ -188,7 +214,9 @@ def do_notify(module, url, payload): def main(): module = AnsibleModule( argument_spec=dict( - webhook_url=dict(type="str", required=True, no_log=True), + space=dict(type="str", required=True), + key=dict(type="str", required=True, no_log=True), + token=dict(type="str", required=True, no_log=True), text=dict(type="str", required=True), thread_key=dict(type="str", no_log=False), create_new_thread=dict(type="bool", default=True), @@ -202,7 +230,9 @@ def main(): payload = build_payload(module.params["text"], module.params["thread_key"]) url = build_url( - module.params["webhook_url"], + module.params["space"], + module.params["key"], + module.params["token"], module.params["thread_key"], module.params["create_new_thread"], ) diff --git a/tests/unit/plugins/modules/test_google_chat.py b/tests/unit/plugins/modules/test_google_chat.py index 8288c6c40a..0307f0b1a6 100644 --- a/tests/unit/plugins/modules/test_google_chat.py +++ b/tests/unit/plugins/modules/test_google_chat.py @@ -17,7 +17,10 @@ from ansible_collections.community.internal_test_tools.tests.unit.plugins.module from ansible_collections.community.general.plugins.modules import google_chat -WEBHOOK = "https://chat.googleapis.com/v1/spaces/SPACE_ID/messages?key=KEY&token=TOKEN" +SPACE = "SPACE_ID" +KEY = "KEY" +TOKEN = "TOKEN" +BASE = "https://chat.googleapis.com/v1/spaces/SPACE_ID/messages" def make_response(payload): @@ -42,20 +45,20 @@ class TestGoogleChatModule(ModuleTestCase): self.module.main() def test_missing_text(self): - """Failure when webhook_url is given but text is missing""" - with set_module_args({"webhook_url": WEBHOOK}): + """Failure when connection params are given but text is missing""" + with set_module_args({"space": SPACE, "key": KEY, "token": TOKEN}): with self.assertRaises(AnsibleFailJson): self.module.main() - def test_missing_webhook_url(self): - """Failure when text is given but webhook_url is missing""" - with set_module_args({"text": "test"}): + def test_missing_space(self): + """Failure when text is given but space is missing""" + with set_module_args({"key": KEY, "token": TOKEN, "text": "test"}): with self.assertRaises(AnsibleFailJson): self.module.main() def test_successful_message(self): """tests sending a plain message""" - with set_module_args({"webhook_url": WEBHOOK, "text": "test"}): + with set_module_args({"space": SPACE, "key": KEY, "token": TOKEN, "text": "test"}): with patch.object(google_chat, "fetch_url") as fetch_url_mock: fetch_url_mock.return_value = ( make_response({"name": "spaces/AAAA/messages/BBBB.BBBB"}), @@ -68,14 +71,18 @@ class TestGoogleChatModule(ModuleTestCase): call_data = json.loads(fetch_url_mock.call_args[1]["data"]) assert call_data["text"] == "test" assert "thread" not in call_data - assert fetch_url_mock.call_args[1]["url"] == WEBHOOK + url = fetch_url_mock.call_args[1]["url"] + assert url.startswith(BASE + "?") + assert "key=KEY" in url + assert "token=TOKEN" in url + assert "messageReplyOption" not in url assert fetch_url_mock.call_args[1]["method"] == "POST" assert result.exception.args[0]["changed"] assert result.exception.args[0]["name"] == "spaces/AAAA/messages/BBBB.BBBB" def test_failed_message(self): """tests failing to send a message (non-200 response)""" - with set_module_args({"webhook_url": WEBHOOK, "text": "test"}): + with set_module_args({"space": SPACE, "key": KEY, "token": TOKEN, "text": "test"}): with patch.object(google_chat, "fetch_url") as fetch_url_mock: fetch_url_mock.return_value = ( None, @@ -89,7 +96,7 @@ class TestGoogleChatModule(ModuleTestCase): def test_message_with_thread(self): """tests sending a message with a thread_key and reading back the thread name""" - with set_module_args({"webhook_url": WEBHOOK, "text": "test", "thread_key": "deploy-1"}): + with set_module_args({"space": SPACE, "key": KEY, "token": TOKEN, "text": "test", "thread_key": "deploy-1"}): with patch.object(google_chat, "fetch_url") as fetch_url_mock: fetch_url_mock.return_value = ( make_response( @@ -109,14 +116,16 @@ class TestGoogleChatModule(ModuleTestCase): assert call_data["thread"]["threadKey"] == "deploy-1" assert result.exception.args[0]["thread_name"] == "spaces/AAAA/threads/CCCC" - def test_create_new_thread_option(self): - """message_reply_option must be added as a query parameter with & (webhook already has ?)""" + def test_create_new_thread_false_appends_reply_or_fail(self): + """create_new_thread=false must map to REPLY_MESSAGE_OR_FAIL in the URL""" with set_module_args( { - "webhook_url": WEBHOOK, + "space": SPACE, + "key": KEY, + "token": TOKEN, "text": "test", "thread_key": "deploy-1", - "message_reply_option": "REPLY_MESSAGE_OR_FAIL", + "create_new_thread": False, } ): with patch.object(google_chat, "fetch_url") as fetch_url_mock: @@ -132,7 +141,7 @@ class TestGoogleChatModule(ModuleTestCase): def test_check_mode(self): """check mode reports changed and never calls the API""" - with set_module_args({"webhook_url": WEBHOOK, "text": "test", "_ansible_check_mode": True}): + with set_module_args({"space": SPACE, "key": KEY, "token": TOKEN, "text": "test", "_ansible_check_mode": True}): with patch.object(google_chat, "fetch_url") as fetch_url_mock: with self.assertRaises(AnsibleExitJson) as result: self.module.main() @@ -153,16 +162,18 @@ def test_build_payload_with_thread(): def test_build_url_without_thread(): - url = google_chat.build_url(WEBHOOK, None, True) - assert url.startswith(WEBHOOK + "?") + url = google_chat.build_url(SPACE, KEY, TOKEN, None, True) + assert url.startswith(BASE + "?") + assert "key=KEY" in url + assert "token=TOKEN" in url assert "messageReplyOption" not in url def test_build_url_create_new_thread_true(): - url = google_chat.build_url(WEBHOOK, "deploy-1", True) + url = google_chat.build_url(SPACE, KEY, TOKEN, "deploy-1", True) assert "messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD" in url def test_build_url_create_new_thread_false(): - url = google_chat.build_url(WEBHOOK, "deploy-1", False) + url = google_chat.build_url(SPACE, KEY, TOKEN, "deploy-1", False) assert "messageReplyOption=REPLY_MESSAGE_OR_FAIL" in url