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. :)


Discussion:
Hey @pmarkert, thanks for taking the time to report this issue. We have an assumption that developers pass an array of either dict or `slack_sdk.models` objects for `blocks` and `attachments` but there is no mention of this in the source code comment and the documents. Also, methods of WebClient do not have any validation of input types. I call this a bug and we will quickly fix the warning logic to work with a serialized `attachments` str value. 

@eddyg I'm going to work on this later today in my time but if you have time and are interested in resolving this, please let me know. I'm happy to wait for your pull request and be a reviewer.
Sorry about that, Phillip. I submitted the PR that tried to improve the warning logic around attachments, and wasn't aware that strings "just worked" when passed to the `attachments` parameter. I've submitted a PR that has what I think is enough to fix this issue:

Before:

```
In [23]: attachments = json.dumps([{'text': 'text'}])

In [24]: kwargs = {'ts': 123456, 'text': 'test', 'attachments': attachments}

In [25]: _warn_if_text_is_missing('test', kwargs)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-25-042f8a7c1899> in <module>
----> 1 _warn_if_text_is_missing('test', kwargs)

<ipython-input-21-6d1821817960> in _warn_if_text_is_missing(endpoint, kwargs)
      3     if attachments:
      4         if all(
----> 5             [
      6                 attachment.get("fallback")
      7                 and len(attachment.get("fallback").strip()) != 0

<ipython-input-21-6d1821817960> in <listcomp>(.0)
      4         if all(
      5             [
----> 6                 attachment.get("fallback")
      7                 and len(attachment.get("fallback").strip()) != 0
      8                 for attachment in attachments

AttributeError: 'str' object has no attribute 'get'
```

which replicates the behavior you say. With the fix:

```
In [27]: _warn_if_text_is_missing('test', kwargs)

In [28]:
```

No error now. Hopefully this is all that's needed.

Again, my apologies!
We will release a new patch version shortly.
I just released v3.4.2 - thanks again for reporting this issue 🙇 



Patch:
diff --git a/slack_sdk/web/internal_utils.py b/slack_sdk/web/internal_utils.py
index 99a85b589..6488838e7 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 000000000..2eea4ae65
--- /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"])