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

Issue:
ImageBlock title parsing defaults to incorrect behaviour when given a string
When providing a `str` as the `title` argument, the block created is incorrect and when submitted to Slack (e.g. via a `views.open` API call) will hit a JSON Schema error:
```
[ERROR] failed to match all allowed schemas [json-pointer:/view/blocks/N/title]
[ERROR] must be a valid enum value [json-pointer:/view/blocks/N/title/type]
```

This is easily visible in the shell by doing:
```python
In [1]: from slack_sdk.models.blocks import ImageBlock
In [2]: img = ImageBlock(image_url="https://example.com/", alt_text="example", title="example")
In [3]: img.title
Out[3]: <slack_sdk.MarkdownTextObject: {'text': 'example', 'type': 'mrkdwn'}>
```
See how it's been parsed into `mrkdwn`? 

Based on the _doctstring_ of the `ImageBlock.__init__`, which says:
> title: An optional title for the image in the form of a text object that can only be of type: `plain_text.`

_and_ the official [documentation](https://api.slack.com/reference/block-kit/blocks#image_fields) which says:
> An optional title for the image in the form of a text object that can only be of type: `plain_text`. Maximum length for the text in this field is `2000` characters.

The current behaviour seems to be incorrect. 
I believe that the fix would be to do:
```diff
- self.title = TextObject.parse(title)
+ self.title = TextObject.parse(title, default_type=PlainTextObject.type)
```
which is already the behaviour in other places (e.g. `InputBlock` label & hint parsing, `HeaderBlock`, ...)


Pull request:
Fix #1369 ImageBlock title parsing defaults to incorrect behaviour when given a string
## Summary

This pull request resolves #1369 

### Category (place an `x` in each of the `[ ]`)

- [ ] **slack_sdk.web.WebClient (sync/async)** (Web API client)
- [ ] **slack_sdk.webhook.WebhookClient (sync/async)** (Incoming Webhook, response_url sender)
- [ ] **slack_sdk.socket_mode** (Socket Mode client)
- [ ] **slack_sdk.signature** (Request Signature Verifier)
- [ ] **slack_sdk.oauth** (OAuth Flow Utilities)
- [x] **slack_sdk.models** (UI component builders)
- [ ] **slack_sdk.scim** (SCIM API client)
- [ ] **slack_sdk.audit_logs** (Audit Logs API client)
- [ ] **slack_sdk.rtm_v2** (RTM client)
- [ ] `/docs-src` (Documents, have you run `./scripts/docs.sh`?)
- [ ] `/docs-src-v2` (Documents, have you run `./scripts/docs-v2.sh`?)
- [ ] `/tutorial` (PythOnBoardingBot tutorial)
- [x] `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 `python3 -m venv .venv && source .venv/bin/activate && ./scripts/run_validation.sh` after making the changes.


Patch:
diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml
index acbd048..1b42cc4 100644
--- a/.github/workflows/ci-build.yml
+++ b/.github/workflows/ci-build.yml
@@ -18,9 +18,9 @@ jobs:
       PYTHON_SLACK_SDK_MOCK_SERVER_MODE: 'threading'
       CI_LARGE_SOCKET_MODE_PAYLOAD_TESTING_DISABLED: '1'
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: ${{ matrix.python-version }}
     - name: Install dependencies
diff --git a/.github/workflows/pytype.yml b/.github/workflows/pytype.yml
index 96eb6b2..ddd36c1 100644
--- a/.github/workflows/pytype.yml
+++ b/.github/workflows/pytype.yml
@@ -13,9 +13,9 @@ jobs:
       matrix:
         python-version: ['3.9']
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: ${{ matrix.python-version }}
     - name: Install dependencies
diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py
index c38ecb0..22ed4f3 100644
--- a/slack_sdk/models/blocks/blocks.py
+++ b/slack_sdk/models/blocks/blocks.py
@@ -15,6 +15,7 @@ from .block_elements import BlockElement
 from .block_elements import ImageElement
 from .block_elements import InputInteractiveElement
 from .block_elements import InteractiveElement
+from ...errors import SlackObjectFormationError
 
 
 # -------------------------------------------------
@@ -216,7 +217,7 @@ class ImageBlock(Block):
         *,
         image_url: str,
         alt_text: str,
-        title: Optional[Union[str, dict, TextObject]] = None,
+        title: Optional[Union[str, dict, PlainTextObject]] = None,
         block_id: Optional[str] = None,
         **others: dict,
     ):
@@ -240,7 +241,19 @@ class ImageBlock(Block):
 
         self.image_url = image_url
         self.alt_text = alt_text
-        self.title = TextObject.parse(title)
+        parsed_title = None
+        if title is not None:
+            if isinstance(title, str):
+                parsed_title = PlainTextObject(text=title)
+            elif isinstance(title, dict):
+                if title.get("type") != PlainTextObject.type:
+                    raise SlackObjectFormationError(f"Unsupported type for title in an image block: {title.get('type')}")
+                parsed_title = PlainTextObject(text=title.get("text"), emoji=title.get("emoji"))
+            elif isinstance(title, PlainTextObject):
+                parsed_title = title
+            else:
+                raise SlackObjectFormationError(f"Unsupported type for title in an image block: {type(title)}")
+        self.title = parsed_title
 
     @JsonValidator(f"image_url attribute cannot exceed {image_url_max_length} characters")
     def _validate_image_url_length(self):


Test patch:
diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py
index 9eb12a8..5461ba8 100644
--- a/tests/slack_sdk/models/test_blocks.py
+++ b/tests/slack_sdk/models/test_blocks.py
@@ -251,6 +251,60 @@ class ImageBlockTests(unittest.TestCase):
         }
         self.assertDictEqual(input, ImageBlock(**input).to_dict())
 
+    def test_issue_1369_title_type(self):
+        self.assertEqual(
+            "plain_text",
+            ImageBlock(
+                image_url="https://example.com/",
+                alt_text="example",
+                title="example",
+            ).title.type,
+        )
+
+        self.assertEqual(
+            "plain_text",
+            ImageBlock(
+                image_url="https://example.com/",
+                alt_text="example",
+                title={
+                    "type": "plain_text",
+                    "text": "Please enjoy this photo of a kitten",
+                },
+            ).title.type,
+        )
+
+        self.assertEqual(
+            "plain_text",
+            ImageBlock(
+                image_url="https://example.com/",
+                alt_text="example",
+                title=PlainTextObject(text="example"),
+            ).title.type,
+        )
+
+        with self.assertRaises(SlackObjectFormationError):
+            self.assertEqual(
+                "plain_text",
+                ImageBlock(
+                    image_url="https://example.com/",
+                    alt_text="example",
+                    title={
+                        "type": "mrkdwn",
+                        "text": "Please enjoy this photo of a kitten",
+                    },
+                ).title.type,
+            )
+
+            with self.assertRaises(SlackObjectFormationError):
+                self.assertEqual(
+                    "plain_text",
+                    ImageBlock(
+                        image_url="https://example.com/",
+                        alt_text="example",
+                        title=MarkdownTextObject(text="example"),
+                    ).title.type,
+                )
+
     def test_json(self):
         self.assertDictEqual(
             {