Repository: getmoto/moto

Issue:
iot_data: incorrect type hinting for "payload"
The type hinting for the payload in `IoTDataPlaneBackend` appears to be incorrect:
- https://github.com/getmoto/moto/blob/eaa8b8793bb5b922e984ada8e5043b1517a6b8e5/moto/iotdata/models.py#L214
- https://github.com/getmoto/moto/blob/eaa8b8793bb5b922e984ada8e5043b1517a6b8e5/moto/iotdata/models.py#L156

`payload` is typed as `str` but it (always) appears to be `bytes`.

In PR #6509 / commit 2dbbdc3123d092781fb783691d99bddd65dacda2 the code was updated to bytes but the typing wasn't adjusted.

**With a bytes payload:**

```python
from typing import TYPE_CHECKING

import boto3
from moto import iotdata, mock_aws

with mock_aws():
    iot_client = boto3.client("iot-data")
    iot_client.publish(
        topic="foo",
        payload=b"bar",
    )

    mock_backend = iotdata.models.iotdata_backends["123456789012"]["us-east-1"]
    print(mock_backend.published_payloads)
    if TYPE_CHECKING:
        reveal_type(mock_backend.published_payloads)

    (topic, payload) = mock_backend.published_payloads[0]
    if TYPE_CHECKING:
        reveal_type(payload)

    assert topic == "foo"
    assert payload == b"bar"
```

Running it outputs:
```
[('foo', b'bar')]
```
i.e. payload is bytes

Running the mypy or pyright typechecker in strict mode:
```
mock.py:16: note: Revealed type is "builtins.list[tuple[builtins.str, builtins.str]]"
mock.py:20: note: Revealed type is "builtins.str"
mock.py:23: error: Non-overlapping equality check (left operand type: "str", right operand type: "Literal[b'bar']")  [comparison-overlap]
Found 1 error in 1 file (checked 1 source file)
```

**With a str payload:**

```python
from typing import TYPE_CHECKING

import boto3
from moto import iotdata, mock_aws

with mock_aws():
    iot_client = boto3.client("iot-data")
    iot_client.publish(
        topic="foo",
        payload="bar",
    )

    mock_backend = iotdata.models.iotdata_backends["123456789012"]["us-east-1"]
    print(mock_backend.published_payloads)
    if TYPE_CHECKING:
        reveal_type(mock_backend.published_payloads)

    (topic, payload) = mock_backend.published_payloads[0]
    if TYPE_CHECKING:
        reveal_type(payload)

    assert topic == "foo"
    assert payload == "bar"
```

Running:
```
[('foo', b'bar')]
Traceback (most recent call last):
  File ".../mock.py", line 23, in <module>
    assert payload == "bar"
AssertionError
```
Also bytes


(The reason why I was ran into it: I wanted to verify in a unit test that the message was published on IOT with the right payload. The quickest/easiest method that I could find to do that was by inspecting `iotdata.models.iotdata_backends`)



Discussion:

Resolving patch:
diff --git a/moto/iotdata/models.py b/moto/iotdata/models.py
--- a/moto/iotdata/models.py
+++ b/moto/iotdata/models.py
@@ -153,7 +153,7 @@ def to_dict(self, include_delta: bool = True) -> Dict[str, Any]:
 class IoTDataPlaneBackend(BaseBackend):
     def __init__(self, region_name: str, account_id: str):
         super().__init__(region_name, account_id)
-        self.published_payloads: List[Tuple[str, str]] = list()
+        self.published_payloads: List[Tuple[str, bytes]] = list()
 
     @property
     def iot_backend(self) -> IoTBackend:
@@ -211,7 +211,7 @@ def delete_thing_shadow(
         thing.thing_shadows[shadow_name] = new_shadow
         return new_shadow
 
-    def publish(self, topic: str, payload: str) -> None:
+    def publish(self, topic: str, payload: bytes) -> None:
         self.published_payloads.append((topic, payload))
 
     def list_named_shadows_for_thing(self, thing_name: str) -> List[FakeShadow]:


Test patch:
diff --git a/tests/test_iotdata/test_iotdata.py b/tests/test_iotdata/test_iotdata.py
--- a/tests/test_iotdata/test_iotdata.py
+++ b/tests/test_iotdata/test_iotdata.py
@@ -15,7 +15,7 @@
 
 
 @mock_aws
-def test_basic():
+def test_basic() -> None:
     iot_client = boto3.client("iot", region_name="ap-northeast-1")
     client = boto3.client("iot-data", region_name="ap-northeast-1")
     name = "my-thing"
@@ -36,7 +36,7 @@ def test_basic():
 
     res = client.get_thing_shadow(thingName=name)
     payload = json.loads(res["payload"].read())
-    expected_state = b'{"desired": {"led": "on"}, "delta": {"led": "on"}}'
+    expected_state = '{"desired": {"led": "on"}, "delta": {"led": "on"}}'
     assert payload["state"] == json.loads(expected_state)
     assert "led" in payload["metadata"]["desired"]
     assert payload["version"] == 1
@@ -48,7 +48,7 @@ def test_basic():
 
 
 @mock_aws
-def test_update():
+def test_update() -> None:
     iot_client = boto3.client("iot", region_name="ap-northeast-1")
     client = boto3.client("iot-data", region_name="ap-northeast-1")
     name = "my-thing"
@@ -66,7 +66,7 @@ def test_update():
 
     res = client.get_thing_shadow(thingName=name)
     payload = json.loads(res["payload"].read())
-    expected_state = b'{"desired": {"led": "on"}, "delta": {"led": "on"}}'
+    expected_state = '{"desired": {"led": "on"}, "delta": {"led": "on"}}'
     assert payload["state"] == json.loads(expected_state)
     assert "led" in payload["metadata"]["desired"]
     assert payload["version"] == 1
@@ -84,7 +84,7 @@ def test_update():
 
     res = client.get_thing_shadow(thingName=name)
     payload = json.loads(res["payload"].read())
-    expected_state = b'{"desired": {"led": "on"}, "reported": {"led": "on"}}'
+    expected_state = '{"desired": {"led": "on"}, "reported": {"led": "on"}}'
     assert payload["state"] == json.loads(expected_state)
     assert "led" in payload["metadata"]["desired"]
     assert payload["version"] == 2
@@ -98,7 +98,7 @@ def test_update():
 
 
 @mock_aws
-def test_create_named_shadows():
+def test_create_named_shadows() -> None:
     if LooseVersion(boto3_version) < LooseVersion("1.29.0"):
         raise SkipTest("Parameter only available in newer versions")
     iot_client = boto3.client("iot", region_name="ap-northeast-1")
@@ -154,23 +154,25 @@ def test_create_named_shadows():
 
 
 @mock_aws
-def test_publish():
+def test_publish() -> None:
     region_name = "ap-northeast-1"
     client = boto3.client("iot-data", region_name=region_name)
     client.publish(topic="test/topic1", qos=1, payload=b"pl1")
     client.publish(topic="test/topic2", qos=1, payload=b"pl2")
     client.publish(topic="test/topic3", qos=1, payload=b"\xbf")
+    client.publish(topic="test/topic4", qos=1, payload="string")
 
     if not settings.TEST_SERVER_MODE:
         mock_backend = moto.iotdata.models.iotdata_backends[ACCOUNT_ID][region_name]
-        assert len(mock_backend.published_payloads) == 3
+        assert len(mock_backend.published_payloads) == 4
         assert ("test/topic1", b"pl1") in mock_backend.published_payloads
         assert ("test/topic2", b"pl2") in mock_backend.published_payloads
         assert ("test/topic3", b"\xbf") in mock_backend.published_payloads
+        assert ("test/topic4", b"string") in mock_backend.published_payloads
 
 
 @mock_aws
-def test_delete_field_from_device_shadow():
+def test_delete_field_from_device_shadow() -> None:
     test_thing_name = "TestThing"
 
     iot_raw_client = boto3.client("iot", region_name="eu-central-1")
diff --git a/tests/test_iotdata/test_server.py b/tests/test_iotdata/test_server.py
--- a/tests/test_iotdata/test_server.py
+++ b/tests/test_iotdata/test_server.py
@@ -11,7 +11,7 @@
 
 
 @mock_aws
-def test_iotdata_list():
+def test_iotdata_list() -> None:
     backend = server.create_backend_app("iot-data")
     test_client = backend.test_client()
 
@@ -29,7 +29,7 @@ def test_iotdata_list():
     ],
 )
 @mock_aws
-def test_publish(url_encode_topic):
+def test_publish(url_encode_topic: bool) -> None:
     backend = server.create_backend_app("iot-data")
     test_client = backend.test_client()