# Copyright (c) 2015 Nicolas JOUANIN # # See the file license.txt for copying permission. import sys import asyncio import logging import unittest from unittest.mock import patch, call, MagicMock import pytest from hbmqtt.adapters import StreamReaderAdapter, StreamWriterAdapter from hbmqtt.broker import ( EVENT_BROKER_PRE_START, EVENT_BROKER_POST_START, EVENT_BROKER_PRE_SHUTDOWN, EVENT_BROKER_POST_SHUTDOWN, EVENT_BROKER_CLIENT_CONNECTED, EVENT_BROKER_CLIENT_DISCONNECTED, EVENT_BROKER_CLIENT_SUBSCRIBED, EVENT_BROKER_CLIENT_UNSUBSCRIBED, EVENT_BROKER_MESSAGE_RECEIVED, Broker) from hbmqtt.client import MQTTClient, ConnectException from hbmqtt.mqtt import ( ConnectPacket, ConnackPacket, PublishPacket, PubrecPacket, PubrelPacket, PubcompPacket, DisconnectPacket) from hbmqtt.mqtt.connect import ConnectVariableHeader, ConnectPayload from hbmqtt.mqtt.constants import QOS_0, QOS_1, QOS_2 formatter = "[%(asctime)s] %(name)s {%(filename)s:%(lineno)d} %(levelname)s - %(message)s" logging.basicConfig(level=logging.DEBUG, format=formatter) log = logging.getLogger(__name__) # monkey patch MagicMock # taken from https://stackoverflow.com/questions/51394411/python-object-magicmock-cant-be-used-in-await-expression async def async_magic(): pass MagicMock.__await__ = lambda x: async_magic().__await__() @pytest.mark.asyncio async def test_start_stop(broker, mock_plugin_manager): mock_plugin_manager.assert_has_calls( [call().fire_event(EVENT_BROKER_PRE_START), call().fire_event(EVENT_BROKER_POST_START)], any_order=True) mock_plugin_manager.reset_mock() await broker.shutdown() mock_plugin_manager.assert_has_calls( [call().fire_event(EVENT_BROKER_PRE_SHUTDOWN), call().fire_event(EVENT_BROKER_POST_SHUTDOWN)], any_order=True) assert broker.transitions.is_stopped() @pytest.mark.asyncio async def test_client_connect(broker, mock_plugin_manager): client = MQTTClient() ret = await client.connect('mqtt://127.0.0.1/') assert ret == 0 assert client.session.client_id in broker._sessions await client.disconnect() await asyncio.sleep(0.01) mock_plugin_manager.assert_has_calls( [call().fire_event(EVENT_BROKER_CLIENT_CONNECTED, client_id=client.session.client_id), call().fire_event(EVENT_BROKER_CLIENT_DISCONNECTED, client_id=client.session.client_id)], any_order=True) @pytest.mark.asyncio async def test_client_connect_will_flag(broker, event_loop): conn_reader, conn_writer = \ await asyncio.open_connection('127.0.0.1', 1883, loop=event_loop) reader = StreamReaderAdapter(conn_reader) writer = StreamWriterAdapter(conn_writer) vh = ConnectVariableHeader() payload = ConnectPayload() vh.keep_alive = 10 vh.clean_session_flag = False vh.will_retain_flag = False vh.will_flag = True vh.will_qos = QOS_0 payload.client_id = 'test_id' payload.will_message = b'test' payload.will_topic = '/topic' connect = ConnectPacket(vh=vh, payload=payload) await connect.to_stream(writer) await ConnackPacket.from_stream(reader) await asyncio.sleep(0.1) disconnect = DisconnectPacket() await disconnect.to_stream(writer) await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_client_connect_clean_session_false(broker): client = MQTTClient(client_id="", config={'auto_reconnect': False}) return_code = None try: await client.connect('mqtt://127.0.0.1/', cleansession=False) except ConnectException as ce: return_code = ce.return_code assert return_code == 0x02 assert client.session.client_id not in broker._sessions await client.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_client_subscribe(broker, mock_plugin_manager): client = MQTTClient() ret = await client.connect('mqtt://127.0.0.1/') assert ret == 0 await client.subscribe([('/topic', QOS_0)]) # Test if the client test client subscription is registered assert '/topic' in broker._subscriptions subs = broker._subscriptions['/topic'] assert len(subs) == 1 (s, qos) = subs[0] assert s == client.session assert qos == QOS_0 await client.disconnect() await asyncio.sleep(0.1) mock_plugin_manager.assert_has_calls( [call().fire_event(EVENT_BROKER_CLIENT_SUBSCRIBED, client_id=client.session.client_id, topic='/topic', qos=QOS_0)], any_order=True) @pytest.mark.asyncio async def test_client_subscribe_twice(broker, mock_plugin_manager): client = MQTTClient() ret = await client.connect('mqtt://127.0.0.1/') assert ret == 0 await client.subscribe([('/topic', QOS_0)]) # Test if the client test client subscription is registered assert '/topic' in broker._subscriptions subs = broker._subscriptions['/topic'] assert len(subs) == 1 (s, qos) = subs[0] assert s == client.session assert qos == QOS_0 await client.subscribe([('/topic', QOS_0)]) assert len(subs) == 1 (s, qos) = subs[0] assert s == client.session assert qos == QOS_0 await client.disconnect() await asyncio.sleep(0.1) mock_plugin_manager.assert_has_calls( [call().fire_event(EVENT_BROKER_CLIENT_SUBSCRIBED, client_id=client.session.client_id, topic='/topic', qos=QOS_0)], any_order=True) @pytest.mark.asyncio async def test_client_unsubscribe(broker, mock_plugin_manager): client = MQTTClient() ret = await client.connect('mqtt://127.0.0.1/') assert ret == 0 await client.subscribe([('/topic', QOS_0)]) # Test if the client test client subscription is registered assert '/topic' in broker._subscriptions subs = broker._subscriptions['/topic'] assert len(subs) == 1 (s, qos) = subs[0] assert s == client.session assert qos == QOS_0 await client.unsubscribe(['/topic']) await asyncio.sleep(0.1) assert broker._subscriptions['/topic'] == [] await client.disconnect() await asyncio.sleep(0.1) mock_plugin_manager.assert_has_calls( [ call().fire_event(EVENT_BROKER_CLIENT_SUBSCRIBED, client_id=client.session.client_id, topic='/topic', qos=QOS_0), call().fire_event(EVENT_BROKER_CLIENT_UNSUBSCRIBED, client_id=client.session.client_id, topic='/topic') ], any_order=True) @pytest.mark.asyncio async def test_client_publish(broker, mock_plugin_manager): pub_client = MQTTClient() ret = await pub_client.connect('mqtt://127.0.0.1/') assert ret == 0 ret_message = await pub_client.publish('/topic', b'data', QOS_0) await pub_client.disconnect() assert broker._retained_messages == {} await asyncio.sleep(0.1) mock_plugin_manager.assert_has_calls( [ call().fire_event(EVENT_BROKER_MESSAGE_RECEIVED, client_id=pub_client.session.client_id, message=ret_message), ], any_order=True) @pytest.mark.asyncio async def test_client_publish_dup(broker, event_loop): conn_reader, conn_writer = \ await asyncio.open_connection('127.0.0.1', 1883, loop=event_loop) reader = StreamReaderAdapter(conn_reader) writer = StreamWriterAdapter(conn_writer) vh = ConnectVariableHeader() payload = ConnectPayload() vh.keep_alive = 10 vh.clean_session_flag = False vh.will_retain_flag = False payload.client_id = 'test_id' connect = ConnectPacket(vh=vh, payload=payload) await connect.to_stream(writer) await ConnackPacket.from_stream(reader) publish_1 = PublishPacket.build('/test', b'data', 1, False, QOS_2, False) await publish_1.to_stream(writer) asyncio.ensure_future(PubrecPacket.from_stream(reader), loop=event_loop) await asyncio.sleep(2) publish_dup = PublishPacket.build('/test', b'data', 1, True, QOS_2, False) await publish_dup.to_stream(writer) await PubrecPacket.from_stream(reader) pubrel = PubrelPacket.build(1) await pubrel.to_stream(writer) await PubcompPacket.from_stream(reader) disconnect = DisconnectPacket() await disconnect.to_stream(writer) @pytest.mark.asyncio async def test_client_publish_invalid_topic(broker): assert broker.transitions.is_started() pub_client = MQTTClient() ret = await pub_client.connect('mqtt://127.0.0.1/') assert ret == 0 await pub_client.publish('/+', b'data', QOS_0) await asyncio.sleep(0.1) await pub_client.disconnect() @pytest.mark.asyncio async def test_client_publish_big(broker, mock_plugin_manager): pub_client = MQTTClient() ret = await pub_client.connect('mqtt://127.0.0.1/') assert ret == 0 ret_message = await pub_client.publish('/topic', bytearray(b'\x99' * 256 * 1024), QOS_2) await pub_client.disconnect() assert broker._retained_messages == {} await asyncio.sleep(0.1) mock_plugin_manager.assert_has_calls( [ call().fire_event(EVENT_BROKER_MESSAGE_RECEIVED, client_id=pub_client.session.client_id, message=ret_message), ], any_order=True) @pytest.mark.asyncio async def test_client_publish_retain(broker): pub_client = MQTTClient() ret = await pub_client.connect('mqtt://127.0.0.1/') assert ret == 0 await pub_client.publish('/topic', b'data', QOS_0, retain=True) await pub_client.disconnect() await asyncio.sleep(0.1) assert '/topic' in broker._retained_messages retained_message = broker._retained_messages['/topic'] assert retained_message.source_session == pub_client.session assert retained_message.topic == '/topic' assert retained_message.data == b'data' assert retained_message.qos == QOS_0 @pytest.mark.asyncio async def test_client_publish_retain_delete(broker): pub_client = MQTTClient() ret = await pub_client.connect('mqtt://127.0.0.1/') assert ret == 0 await pub_client.publish('/topic', b'', QOS_0, retain=True) await pub_client.disconnect() await asyncio.sleep(0.1) assert '/topic' not in broker._retained_messages @pytest.mark.asyncio async def test_client_subscribe_publish(broker): sub_client = MQTTClient() await sub_client.connect('mqtt://127.0.0.1') ret = await sub_client.subscribe([('/qos0', QOS_0), ('/qos1', QOS_1), ('/qos2', QOS_2)]) assert ret == [QOS_0, QOS_1, QOS_2] await _client_publish('/qos0', b'data', QOS_0) await _client_publish('/qos1', b'data', QOS_1) await _client_publish('/qos2', b'data', QOS_2) await asyncio.sleep(0.1) for qos in [QOS_0, QOS_1, QOS_2]: message = await sub_client.deliver_message() assert message is not None assert message.topic == '/qos%s' % qos assert message.data == b'data' assert message.qos == qos await sub_client.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_client_subscribe_invalid(broker): sub_client = MQTTClient() await sub_client.connect('mqtt://127.0.0.1') ret = await sub_client.subscribe( [('+', QOS_0), ('+/tennis/#', QOS_0), ('sport+', QOS_0), ('sport/+/player1', QOS_0)]) assert ret == [QOS_0, QOS_0, 0x80, QOS_0] await asyncio.sleep(0.1) await sub_client.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_client_subscribe_publish_dollar_topic_1(broker): assert broker.transitions.is_started() sub_client = MQTTClient() await sub_client.connect('mqtt://127.0.0.1') ret = await sub_client.subscribe([('#', QOS_0)]) assert ret == [QOS_0] await _client_publish('/topic', b'data', QOS_0) message = await sub_client.deliver_message() assert message is not None await _client_publish('$topic', b'data', QOS_0) await asyncio.sleep(0.1) message = None try: message = await sub_client.deliver_message(timeout=2) except Exception as e: pass assert message is None await sub_client.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def test_client_subscribe_publish_dollar_topic_2(broker): sub_client = MQTTClient() await sub_client.connect('mqtt://127.0.0.1') ret = await sub_client.subscribe([('+/monitor/Clients', QOS_0)]) assert ret == [QOS_0] await _client_publish('/test/monitor/Clients', b'data', QOS_0) message = await sub_client.deliver_message() assert message is not None await _client_publish('$SYS/monitor/Clients', b'data', QOS_0) await asyncio.sleep(0.1) message = None try: message = await sub_client.deliver_message(timeout=2) except Exception as e: pass assert message is None await sub_client.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio @pytest.mark.xfail(reason="see https://github.com/Yakifo/aio-hbmqtt/issues/16", strict=False) async def test_client_publish_retain_subscribe(broker): sub_client = MQTTClient() await sub_client.connect('mqtt://127.0.0.1', cleansession=False) ret = await sub_client.subscribe([('/qos0', QOS_0), ('/qos1', QOS_1), ('/qos2', QOS_2)]) assert ret == [QOS_0, QOS_1, QOS_2] await sub_client.disconnect() await asyncio.sleep(0.1) await _client_publish('/qos0', b'data', QOS_0, retain=True) await _client_publish('/qos1', b'data', QOS_1, retain=True) await _client_publish('/qos2', b'data', QOS_2, retain=True) await sub_client.reconnect() for qos in [QOS_0, QOS_1, QOS_2]: log.debug("TEST QOS: %d" % qos) message = await sub_client.deliver_message() log.debug("Message: " + repr(message.publish_packet)) assert message is not None assert message.topic == '/qos%s' % qos assert message.data == b'data' assert message.qos == qos await sub_client.disconnect() await asyncio.sleep(0.1) @pytest.mark.asyncio async def _client_publish(topic, data, qos, retain=False): pub_client = MQTTClient() ret = await pub_client.connect('mqtt://127.0.0.1/') assert ret == 0 ret = await pub_client.publish(topic, data, qos, retain) await pub_client.disconnect() return ret