Repository: slackapi/python-slack-sdk (python)

Issue:
3.4.1 compatibility break with serialized attachments parameter in chat.* method calls
Broken compatibility warning

### Reproducible in:

#### The Slack SDK version

slack-sdk==3.4.1

#### Python runtime version

Python 3.9.2

#### OS info

ProductName:	Mac OS X
ProductVersion:	10.15.7
BuildVersion:	19H114
Darwin Kernel Version 19.6.0: Tue Nov 10 00:10:30 PST 2020; root:xnu-6153.141.10~1/RELEASE_X86_64

#### Steps to reproduce:

```
    response = client.chat_postMessage(
        channel=channel,
        ts=messageTs,
        text=text,
        blocks=json.dumps(blocks),
        attachments=json.dumps(attachments)
    )
```

### Expected result:

This was working just fine until I updated the sdk from 3.4.0 to 3.4.1. I realize the json.dumps should not be required (and indeed, I prefer the version of the code without it), so it is an easy fix for me and a good lesson learned, but it might catch others by surprise since it is a patch version number, not a minor version.

### Actual result:

```
Traceback (most recent call last):
...SNIP...
  File "/usr/local/lib/python3.9/site-packages/slack_sdk/web/client.py", line 1185, in chat_update
    "Developer": ":large_blue_square:  Developer",
    _warn_if_text_is_missing("chat.update", kwargs)
  File "/usr/local/lib/python3.9/site-packages/slack_sdk/web/internal_utils.py", line 237, in _warn_if_text_is_missing
    [
  File "/usr/local/lib/python3.9/site-packages/slack_sdk/web/internal_utils.py", line 238, in <listcomp>
    attachment.get("fallback")
AttributeError: 'str' object has no attribute 'get'
```

### Requirements

I'm not sure if the acceptance of the JSON encoded parameter before was expected, accidental, or required, but in any case, it worked and now it doesn't. Rolling back to the 3.4.0 version caused it to work again. Once I removed the json.dumps for those two parameters, it began working in 3.4.1, but with a warning message about the missing "fallback" attribute. The issue seems to be caused by changes from 57716b74c3acc9be1ce7bdb1f42a8485c53ab3ab. I'm happy enough with my fixes, but figured someone else may run into this, so I'm hoping that this report can help them troubleshoot more quickly. :)

Pull request:
Prevent breaking change when 'attachments' is a string.
Fixes #971.

Sorry about that. 😞

## Summary

(Describe the goal of this PR. Mention any related issue numbers)

### Category (place an `x` in each of the `[ ]`)

- [X] **slack_sdk.web.WebClient (sync/async)** (Web API client)
- [ ] **slack_sdk.webhook.WebhookClient (sync/async)** (Incoming Webhook, response_url sender)
- [ ] **slack_sdk.models** (UI component builders)
- [ ] **slack_sdk.oauth** (OAuth Flow Utilities)
- [ ] **slack_sdk.socket_mode** (Socket Mode client)
- [ ] **slack_sdk.audit_logs** (Audit Logs API client)
- [ ] **slack_sdk.scim** (SCIM API client)
- [ ] **slack_sdk.rtm** (RTM client)
- [ ] **slack_sdk.signature** (Request Signature Verifier)
- [ ] `/docs-src` (Documents, have you run `./docs.sh`?)
- [ ] `/docs-src-v2` (Documents, have you run `./docs-v2.sh`?)
- [ ] `/tutorial` (PythOnBoardingBot tutorial)
- [ ] `tests`/`integration_tests` (Automated tests for this library)

## Requirements (place an `x` in each `[ ]`)

- [X] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and have done my best effort to follow them.
- [X] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
- [X] I've run `python setup.py validate` after making the changes.


Patch:
diff --git a/slack_sdk/web/internal_utils.py b/slack_sdk/web/internal_utils.py
index 99a85b5..6488838 100644
--- a/slack_sdk/web/internal_utils.py
+++ b/slack_sdk/web/internal_utils.py
@@ -232,7 +232,7 @@ def _to_0_or_1_if_bool(v: Any) -> Union[Any, str]:
 
 def _warn_if_text_is_missing(endpoint: str, kwargs: Dict[str, Any]) -> None:
     attachments = kwargs.get("attachments")
-    if attachments:
+    if attachments and isinstance(attachments, list):
         if all(
             [
                 attachment.get("fallback")


Test patch:
diff --git a/tests/slack_sdk/web/test_web_client_issue_971.py b/tests/slack_sdk/web/test_web_client_issue_971.py
new file mode 100644
index 0000000..2eea4ae
--- /dev/null
+++ b/tests/slack_sdk/web/test_web_client_issue_971.py
@@ -0,0 +1,89 @@
+import json
+import unittest
+
+from slack_sdk.web import WebClient
+from tests.slack_sdk.web.mock_web_api_server import (
+    setup_mock_web_api_server,
+    cleanup_mock_web_api_server,
+)
+
+
+class TestWebClient_Issue_971(unittest.TestCase):
+    def setUp(self):
+        setup_mock_web_api_server(self)
+
+    def tearDown(self):
+        cleanup_mock_web_api_server(self)
+
+    def test_text_arg_only(self):
+        client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111")
+        resp = client.chat_postMessage(channel="C111", text="test")
+        self.assertTrue(resp["ok"])
+
+    def test_blocks_with_text_arg(self):
+        client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111")
+        resp = client.chat_postMessage(channel="C111", text="test", blocks=[])
+        self.assertTrue(resp["ok"])
+
+    def test_blocks_without_text_arg(self):
+        client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111")
+        with self.assertWarns(UserWarning):
+            resp = client.chat_postMessage(channel="C111", blocks=[])
+        self.assertTrue(resp["ok"])
+
+    def test_attachments_with_fallback(self):
+        client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111")
+        resp = client.chat_postMessage(channel="C111", attachments=[{'fallback': 'test'}])
+        self.assertTrue(resp["ok"])
+
+    def test_attachments_with_empty_fallback(self):
+        client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111")
+        with self.assertWarns(UserWarning):
+            resp = client.chat_postMessage(channel="C111", attachments=[{'fallback': ''}])
+        self.assertTrue(resp["ok"])
+
+    def test_attachments_without_fallback(self):
+        client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111")
+        with self.assertWarns(UserWarning):
+            resp = client.chat_postMessage(channel="C111", attachments=[{}])
+        self.assertTrue(resp["ok"])
+
+    def test_attachments_without_fallback_with_text_arg(self):
+        client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111")
+        # this warns because each attachment should have its own fallback, even with "text"
+        with self.assertWarns(UserWarning):
+            resp = client.chat_postMessage(channel="C111", text="test", attachments=[{}])
+        self.assertTrue(resp["ok"])
+
+    def test_multiple_attachments_one_without_fallback(self):
+        client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111")
+        with self.assertWarns(UserWarning):
+            resp = client.chat_postMessage(channel="C111", attachments=[{'fallback': 'test'}, {}])
+        self.assertTrue(resp["ok"])
+
+    def test_blocks_as_deserialzed_json_without_text_arg(self):
+        client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111")
+        # this generates a warning because "text" is missing
+        with self.assertWarns(UserWarning):
+            resp = client.chat_postMessage(channel="C111", attachments=json.dumps([]))
+        self.assertTrue(resp["ok"])
+
+    def test_blocks_as_deserialized_json_with_text_arg(self):
+        client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111")
+        # this DOESN'T warn because the "text" arg is present
+        resp = client.chat_postMessage(channel="C111", text="test", blocks=json.dumps([]))
+        self.assertTrue(resp["ok"])
+
+    def test_attachments_as_deserialzed_json_without_text_arg(self):
+        client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111")
+        # this still generates a warning because "text" is missing. The attachment has already
+        # been deserialized, which isn't explicitly prohibited in the docs (but isn't recommended)
+        with self.assertWarns(UserWarning):
+            resp = client.chat_postMessage(channel="C111", attachments=json.dumps([{"fallback": "test"}]))
+        self.assertTrue(resp["ok"])
+
+    def test_attachments_as_deserialized_json_with_text_arg(self):
+        client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test", team_id="T111")
+        # this DOESN'T warn because the text arg is present (attachment already deserialized)
+        resp = client.chat_postMessage(channel="C111", text="test", attachments=json.dumps([]))
+        self.assertTrue(resp["ok"])