def reauthorize_connection(self, callback):
        """
        Reauthorize connection to the service by disconnecting and then reconnecting using
        fresh credentials.

        This can be called regardless of connection state. If successful, the client will be
        connected. If unsuccessful, the client will be disconnected.

        :param callback: callback which is called when the reauthorization attempt is complete.

        :raises: :class:`azure.iot.device.iothub.pipeline.exceptions.PipelineNotRunning` if the
            pipeline has previously been shut down

        The following exceptions are not "raised", but rather returned via the "error" parameter
        when invoking "callback":

        :raises: :class:`azure.iot.device.iothub.pipeline.exceptions.ConnectionFailedError`
        :raises: :class:`azure.iot.device.iothub.pipeline.exceptions.ConnectionDroppedError`
        :raises: :class:`azure.iot.device.iothub.pipeline.exceptions.UnauthorizedError`
        :raises: :class:`azure.iot.device.iothub.pipeline.exceptions.ProtocolClientError`
        :raises: :class:`azure.iot.device.iothub.pipeline.exceptions.OperationTimeout`
        """
        self._verify_running()
        logger.debug("Starting ReauthorizeConnectionOperation on the pipeline")

        def on_complete(op, error):
            callback(error=error)

        self._pipeline.run_op(
            pipeline_ops_base.ReauthorizeConnectionOperation(callback=on_complete)
        )
    def reauthorize_connection(self, callback):
        """
        Reauthorize connection to the service.

        Technically, this function will return upon disconnection. The disconnection will then
        immediately trigger a reconnect, but this function will not wait for that to return.
        This is (unfortunately) necessary while supporting MQTT3.

        :param callback: callback which is called when the connection to the service has been disconnected

        :raises: :class:`azure.iot.device.iothub.pipeline.exceptions.PipelineNotRunning` if the
            pipeline has previously been shut down

        The following exceptions are not "raised", but rather returned via the "error" parameter
        when invoking "callback":

        :raises: :class:`azure.iot.device.iothub.pipeline.exceptions.ProtocolClientError`
        """
        self._verify_running()
        logger.debug("Starting ReauthorizeConnectionOperation on the pipeline")

        def on_complete(op, error):
            callback(error=error)

        self._pipeline.run_op(
            pipeline_ops_base.ReauthorizeConnectionOperation(
                callback=on_complete))
示例#3
0
class TestMQTTTransportStageExecuteOpWithReauthorizeConnection(
        MQTTTransportStageTestBase, RunOpTests):
    @pytest.mark.it(
        "Sets the ReauthorizeConnectionOperation as the pending connection operation"
    )
    def test_sets_pending_operation(self, stage, create_transport,
                                    op_reauthorize_connection):
        stage.run_op(op_reauthorize_connection)
        assert stage._pending_connection_op is op_reauthorize_connection

    @pytest.mark.it("Cancels any already pending connection operation")
    @pytest.mark.parametrize(
        "pending_connection_op",
        [
            pytest.param(pipeline_ops_base.ConnectOperation(1),
                         id="Pending ConnectOperation"),
            pytest.param(
                pipeline_ops_base.ReauthorizeConnectionOperation(1),
                id="Pending ReauthorizeConnectionOperation",
            ),
            pytest.param(pipeline_ops_base.DisconnectOperation(1),
                         id="Pending DisconnectOperation"),
        ],
    )
    def test_pending_operation_cancelled(self, mocker, stage, create_transport,
                                         op_reauthorize_connection,
                                         pending_connection_op):
        mocker.spy(pending_connection_op, "complete")
        stage._pending_connection_op = pending_connection_op
        stage.run_op(op_reauthorize_connection)

        # Operation has been completed, with an OperationCancelled exception set indicating early cancellation
        assert pending_connection_op.complete.call_count == 1
        assert (type(pending_connection_op.complete.call_args[1]["error"]) is
                pipeline_exceptions.OperationCancelled)

        # New operation is now the pending operation
        assert stage._pending_connection_op is op_reauthorize_connection

    @pytest.mark.it("Does an MQTT reauthorize_connection via the MQTTTransport"
                    )
    def test_mqtt_reauthorize_connection(self, mocker, stage, create_transport,
                                         op_reauthorize_connection):
        stage.run_op(op_reauthorize_connection)
        assert stage.transport.reauthorize_connection.call_count == 1
        assert stage.transport.reauthorize_connection.call_args == mocker.call(
            password=stage.sas_token)

    @pytest.mark.it(
        "Fails the operation and resets the pending connection operation to None, if there is a failure reauthorizing the connection in the MQTTTransport"
    )
    def test_fails_operation(self, mocker, stage, create_transport,
                             op_reauthorize_connection, arbitrary_exception):
        stage.transport.reauthorize_connection.side_effect = arbitrary_exception
        stage.run_op(op_reauthorize_connection)
        assert op_reauthorize_connection.complete.call_count == 1
        assert op_reauthorize_connection.complete.call_args == mocker.call(
            error=arbitrary_exception)
        assert stage._pending_connection_op is None
    def test_fails_pending_reconnect_op(self, mocker, stage, arbitrary_exception):
        # Create a pending ReauthorizeConnectionOperation
        op = pipeline_ops_base.ReauthorizeConnectionOperation(callback=mocker.MagicMock())
        stage.run_op(op)
        assert not op.completed
        assert stage._pending_connection_op is op

        # Trigger connection failure with an arbitrary cause
        stage.transport.on_mqtt_connection_failure_handler(arbitrary_exception)

        assert op.completed
        assert op.error is arbitrary_exception
        assert stage._pending_connection_op is None
示例#5
0
 def test_fails_pending_reauthorize_connection_op(self, mocker, stage,
                                                  create_transport,
                                                  arbitrary_exception):
     op = pipeline_ops_base.ReauthorizeConnectionOperation(
         callback=mocker.MagicMock())
     mocker.spy(op, "complete")
     stage.run_op(op)
     assert op.complete.call_count == 0
     assert stage._pending_connection_op is op
     stage.transport.on_mqtt_connection_failure_handler(arbitrary_exception)
     assert op.complete.call_count == 1
     assert op.complete.call_args == mocker.call(error=arbitrary_exception)
     assert stage._pending_connection_op is None
    def test_completes_pending_reconnect_op(self, mocker, stage):
        # Set a pending reconnect operation
        op = pipeline_ops_base.ReauthorizeConnectionOperation(callback=mocker.MagicMock())
        stage.run_op(op)
        assert not op.completed
        assert stage._pending_connection_op is op

        # Trigger connect completion
        stage.transport.on_mqtt_connected_handler()

        # Reconnect operation completed successfully
        assert op.completed
        assert op.error is None
        assert stage._pending_connection_op is None
class TestMQTTTransportStageOnDisconnected(MQTTTransportStageTestConfigComplex
                                           ):
    @pytest.fixture(params=[False, True],
                    ids=["No error cause", "With error cause"])
    def cause(self, request, arbitrary_exception):
        if request.param:
            return arbitrary_exception
        else:
            return None

    @pytest.mark.it("Sends a DisconnectedEvent up the pipeline")
    @pytest.mark.parametrize(
        "pending_connection_op",
        [
            pytest.param(None, id="No pending operation"),
            pytest.param(
                pipeline_ops_base.ConnectOperation(callback=fake_callback),
                id="Pending ConnectOperation",
            ),
            pytest.param(
                pipeline_ops_base.ReauthorizeConnectionOperation(
                    callback=fake_callback),
                id="Pending ReauthorizeConnectionOperation",
            ),
            pytest.param(
                pipeline_ops_base.DisconnectOperation(callback=fake_callback),
                id="Pending DisconnectOperation",
            ),
        ],
    )
    def test_disconnected_handler(self, stage, pending_connection_op, cause):
        stage._pending_connection_op = pending_connection_op
        assert stage.send_event_up.call_count == 0

        # Trigger disconnect
        stage.transport.on_mqtt_disconnected_handler(cause)

        assert stage.send_event_up.call_count == 1
        event = stage.send_event_up.call_args[0][0]
        assert isinstance(event, pipeline_events_base.DisconnectedEvent)

    @pytest.mark.it("Completes a pending DisconnectOperation successfully")
    def test_compltetes_pending_disconnect_op(self, mocker, stage, cause):
        # Create a pending DisconnectOperation
        op = pipeline_ops_base.DisconnectOperation(callback=mocker.MagicMock())
        stage.run_op(op)
        assert not op.completed
        assert stage._pending_connection_op is op

        # Trigger disconnect
        stage.transport.on_mqtt_disconnected_handler(cause)

        assert op.completed
        assert op.error is None

    @pytest.mark.it(
        "Swallows the exception that caused the disconnect, if there is a pending DisconnectOperation"
    )
    def test_completes_pending_disconnect_op_with_error(
            self, mocker, stage, arbitrary_exception):
        mock_swallow = mocker.patch.object(handle_exceptions,
                                           "swallow_unraised_exception")

        # Create a pending DisconnectOperation
        op = pipeline_ops_base.DisconnectOperation(callback=mocker.MagicMock())
        stage.run_op(op)
        assert not op.completed
        assert stage._pending_connection_op is op

        # Trigger disconnect with arbitrary cause
        stage.transport.on_mqtt_disconnected_handler(arbitrary_exception)

        # Exception swallower was called
        assert mock_swallow.call_count == 1
        assert mock_swallow.call_args == mocker.call(arbitrary_exception,
                                                     log_msg=mocker.ANY)

    @pytest.mark.it(
        "Completes (unsuccessfully) a pending operation that is NOT a DisconnectOperation, with the cause of the disconnection set as the error, if there is a cause provided"
    )
    @pytest.mark.parametrize(
        "pending_connection_op",
        [
            pytest.param(
                pipeline_ops_base.ConnectOperation(callback=fake_callback),
                id="Pending ConnectOperation",
            ),
            pytest.param(
                pipeline_ops_base.ReauthorizeConnectionOperation(
                    callback=fake_callback),
                id="Pending ReauthorizeConnectionOperation",
            ),
        ],
    )
    def test_comletes_with_cause_as_error_if_cause(self, mocker, stage,
                                                   pending_connection_op,
                                                   arbitrary_exception):
        stage._pending_connection_op = pending_connection_op
        assert not pending_connection_op.completed

        # Trigger disconnect with arbitrary cause
        stage.transport.on_mqtt_disconnected_handler(arbitrary_exception)

        assert pending_connection_op.completed
        assert pending_connection_op.error is arbitrary_exception

    @pytest.mark.it(
        "Completes (unsuccessfully) a pending operation that is NOT a DisconnectOperation with a ConnectionDroppedError if no cause is provided for the disconnection"
    )
    @pytest.mark.parametrize(
        "pending_connection_op",
        [
            pytest.param(
                pipeline_ops_base.ConnectOperation(callback=fake_callback),
                id="Pending ConnectOperation",
            ),
            pytest.param(
                pipeline_ops_base.ReauthorizeConnectionOperation(
                    callback=fake_callback),
                id="Pending ReauthorizeConnectionOperation",
            ),
        ],
    )
    def test_comletes_with_connection_dropped_error_as_error_if_no_cause(
            self, mocker, stage, pending_connection_op, arbitrary_exception):
        stage._pending_connection_op = pending_connection_op
        assert not pending_connection_op.completed

        # Trigger disconnect with no cause
        stage.transport.on_mqtt_disconnected_handler()

        assert pending_connection_op.completed
        assert isinstance(pending_connection_op.error,
                          transport_exceptions.ConnectionDroppedError)

    @pytest.mark.it(
        "Sends a ConnectionDroppedError to the background exception handler, if there is no pending operation when a disconnection occurs"
    )
    def test_no_pending_op(self, mocker, stage, cause):
        mock_handler = mocker.patch.object(handle_exceptions,
                                           "handle_background_exception")
        assert stage._pending_connection_op is None

        # Trigger disconnect
        stage.transport.on_mqtt_disconnected_handler(cause)

        assert mock_handler.call_count == 1
        exception = mock_handler.call_args[0][0]
        assert isinstance(exception,
                          transport_exceptions.ConnectionDroppedError)
        assert exception.__cause__ is cause

    @pytest.mark.it("Clears any pending operation on the stage")
    @pytest.mark.parametrize(
        "pending_connection_op",
        [
            pytest.param(None, id="No pending operation"),
            pytest.param(
                pipeline_ops_base.ConnectOperation(callback=fake_callback),
                id="Pending ConnectOperation",
            ),
            pytest.param(
                pipeline_ops_base.ReauthorizeConnectionOperation(
                    callback=fake_callback),
                id="Pending ReauthorizeConnectionOperation",
            ),
            pytest.param(
                pipeline_ops_base.DisconnectOperation(callback=fake_callback),
                id="Pending DisconnectOperation",
            ),
        ],
    )
    def test_clears_pending(self, mocker, stage, pending_connection_op, cause):
        stage._pending_connection_op = pending_connection_op

        # Trigger disconnect
        stage.transport.on_mqtt_disconnected_handler(cause)

        assert stage._pending_connection_op is None
class TestMQTTTransportStageOnConnectionFailure(
        MQTTTransportStageTestConfigComplex):
    @pytest.mark.it("Does not send any events up the pipeline")
    @pytest.mark.parametrize(
        "pending_connection_op",
        [
            pytest.param(None, id="No pending operation"),
            pytest.param(pipeline_ops_base.ConnectOperation(1),
                         id="Pending ConnectOperation"),
            pytest.param(
                pipeline_ops_base.ReauthorizeConnectionOperation(1),
                id="Pending ReauthorizeConnectionOperation",
            ),
            pytest.param(pipeline_ops_base.DisconnectOperation(1),
                         id="Pending DisconnectOperation"),
        ],
    )
    def test_does_not_send_event(self, mocker, stage, pending_connection_op,
                                 arbitrary_exception):
        stage._pending_connection_op = pending_connection_op

        # Trigger connection failure with an arbitrary cause
        stage.transport.on_mqtt_connection_failure_handler(arbitrary_exception)

        assert stage.send_event_up.call_count == 0

    @pytest.mark.it(
        "Completes a pending ConnectOperation unsuccessfully with the cause of connection failure as the error"
    )
    def test_fails_pending_connect_op(self, mocker, stage,
                                      arbitrary_exception):
        # Create a pending ConnectOperation
        op = pipeline_ops_base.ConnectOperation(callback=mocker.MagicMock())
        stage.run_op(op)
        assert not op.completed
        assert stage._pending_connection_op is op

        # Trigger connection failure with an arbitrary cause
        stage.transport.on_mqtt_connection_failure_handler(arbitrary_exception)

        assert op.completed
        assert op.error is arbitrary_exception
        assert stage._pending_connection_op is None

    @pytest.mark.it(
        "Completes a pending ReauthorizeConnectionOperation unsuccessfully with the cause of connection failure as the error"
    )
    def test_fails_pending_reconnect_op(self, mocker, stage,
                                        arbitrary_exception):
        # Create a pending ReauthorizeConnectionOperation
        op = pipeline_ops_base.ReauthorizeConnectionOperation(
            callback=mocker.MagicMock())
        stage.run_op(op)
        assert not op.completed
        assert stage._pending_connection_op is op

        # Trigger connection failure with an arbitrary cause
        stage.transport.on_mqtt_connection_failure_handler(arbitrary_exception)

        assert op.completed
        assert op.error is arbitrary_exception
        assert stage._pending_connection_op is None

    @pytest.mark.it(
        "Ignores a pending DisconnectOperation, and does not complete it")
    def test_ignores_pending_disconnect_op(self, mocker, stage,
                                           arbitrary_exception):
        # Create a pending DisconnectOperation
        op = pipeline_ops_base.DisconnectOperation(callback=mocker.MagicMock())
        stage.run_op(op)
        assert not op.completed
        assert stage._pending_connection_op is op

        # Trigger connection failure with an arbitrary cause
        stage.transport.on_mqtt_connection_failure_handler(arbitrary_exception)

        # Assert nothing changed about the operation
        assert not op.completed
        assert stage._pending_connection_op is op

    @pytest.mark.it(
        "Triggers the background exception handler (with error cause) when the connection failure is unexpected"
    )
    @pytest.mark.parametrize(
        "pending_connection_op",
        [
            pytest.param(None, id="No pending operation"),
            pytest.param(
                pipeline_ops_base.DisconnectOperation(callback=fake_callback),
                id="Pending DisconnectOperation",
            ),
        ],
    )
    def test_unexpected_connection_failure(self, mocker, stage,
                                           arbitrary_exception,
                                           pending_connection_op):
        # A connection failure is unexpected if there is not a pending Connect/ReauthorizeConnection operation
        # i.e. "Why did we get a connection failure? We weren't even trying to connect!"
        mock_handler = mocker.patch.object(handle_exceptions,
                                           "handle_background_exception")
        stage._pending_connection_operation = pending_connection_op

        # Trigger connection failure with arbitrary cause
        stage.transport.on_mqtt_connection_failure_handler(arbitrary_exception)

        # Background exception handler has been called
        assert mock_handler.call_count == 1
        assert mock_handler.call_args == mocker.call(arbitrary_exception)
class TestMQTTTransportStageOnConnected(MQTTTransportStageTestConfigComplex):
    @pytest.mark.it("Sends a ConnectedEvent up the pipeline")
    @pytest.mark.parametrize(
        "pending_connection_op",
        [
            pytest.param(None, id="No pending operation"),
            pytest.param(pipeline_ops_base.ConnectOperation(1),
                         id="Pending ConnectOperation"),
            pytest.param(
                pipeline_ops_base.ReauthorizeConnectionOperation(1),
                id="Pending ReauthorizeConnectionOperation",
            ),
            pytest.param(pipeline_ops_base.DisconnectOperation(1),
                         id="Pending DisconnectOperation"),
        ],
    )
    def test_sends_event_up(self, stage, pending_connection_op):
        stage._pending_connection_op = pending_connection_op
        # Trigger connect completion
        stage.transport.on_mqtt_connected_handler()

        assert stage.send_event_up.call_count == 1
        connect_event = stage.send_event_up.call_args[0][0]
        assert isinstance(connect_event, pipeline_events_base.ConnectedEvent)

    @pytest.mark.it("Completes a pending ConnectOperation successfully")
    def test_completes_pending_connect_op(self, mocker, stage):
        # Set a pending connect operation
        op = pipeline_ops_base.ConnectOperation(callback=mocker.MagicMock())
        stage.run_op(op)
        assert not op.completed
        assert stage._pending_connection_op is op

        # Trigger connect completion
        stage.transport.on_mqtt_connected_handler()

        # Connect operation completed successfully
        assert op.completed
        assert op.error is None
        assert stage._pending_connection_op is None

    @pytest.mark.it(
        "Completes a pending ReauthorizeConnectionOperation successfully")
    def test_completes_pending_reconnect_op(self, mocker, stage):
        # Set a pending reconnect operation
        op = pipeline_ops_base.ReauthorizeConnectionOperation(
            callback=mocker.MagicMock())
        stage.run_op(op)
        assert not op.completed
        assert stage._pending_connection_op is op

        # Trigger connect completion
        stage.transport.on_mqtt_connected_handler()

        # Reconnect operation completed successfully
        assert op.completed
        assert op.error is None
        assert stage._pending_connection_op is None

    @pytest.mark.it(
        "Ignores a pending DisconnectOperation when the transport connected event fires"
    )
    def test_ignores_pending_disconnect_op(self, mocker, stage):
        # Set a pending disconnect operation
        op = pipeline_ops_base.DisconnectOperation(callback=mocker.MagicMock())
        stage.run_op(op)
        assert not op.completed
        assert stage._pending_connection_op is op

        # Trigger connect completion
        stage.transport.on_mqtt_connected_handler()

        # Disconnect operation was NOT completed
        assert not op.completed
        assert stage._pending_connection_op is op
class TestMQTTTransportStageRunOpCalledWithDisconnectOperation(
        MQTTTransportStageTestConfigComplex, StageRunOpTestBase):
    @pytest.fixture
    def op(self, mocker):
        return pipeline_ops_base.DisconnectOperation(
            callback=mocker.MagicMock())

    @pytest.mark.it(
        "Sets the operation as the stage's pending connection operation")
    def test_sets_pending_operation(self, stage, op):
        stage.run_op(op)
        assert stage._pending_connection_op is op

    @pytest.mark.it("Cancels any already pending connection operation")
    @pytest.mark.parametrize(
        "pending_connection_op",
        [
            pytest.param(
                pipeline_ops_base.ConnectOperation(callback=fake_callback),
                id="Pending ConnectOperation",
            ),
            pytest.param(
                pipeline_ops_base.ReauthorizeConnectionOperation(
                    callback=fake_callback),
                id="Pending ReauthorizeConnectionOperation",
            ),
            pytest.param(
                pipeline_ops_base.DisconnectOperation(callback=fake_callback),
                id="Pending DisconnectOperation",
            ),
        ],
    )
    def test_pending_operation_cancelled(self, mocker, stage, op,
                                         pending_connection_op):
        # Set up a pending op
        stage._pending_connection_op = pending_connection_op
        assert not pending_connection_op.completed

        # Run the connect op
        stage.run_op(op)

        # Operation has been completed, with an OperationCancelled exception set indicating early cancellation
        assert pending_connection_op.completed
        assert type(pending_connection_op.error
                    ) is pipeline_exceptions.OperationCancelled

        # New operation is now the pending operation
        assert stage._pending_connection_op is op

    @pytest.mark.it("Performs an MQTT disconnect via the MQTTTransport")
    def test_mqtt_connect(self, mocker, stage, op):
        stage.run_op(op)
        assert stage.transport.disconnect.call_count == 1
        assert stage.transport.disconnect.call_args == mocker.call()

    @pytest.mark.it(
        "Completes the operation unsucessfully if there is a failure disconnecting via the MQTTTransport, using the error raised by the MQTTTransport"
    )
    def test_fails_operation(self, mocker, stage, op, arbitrary_exception):
        stage.transport.disconnect.side_effect = arbitrary_exception
        stage.run_op(op)
        assert op.completed
        assert op.error is arbitrary_exception

    @pytest.mark.it(
        "Resets the stage's pending connection operation to None, if there is a failure disconnecting via the MQTTTransport"
    )
    def test_clears_pending_op_on_failure(self, mocker, stage, op,
                                          arbitrary_exception):
        stage.transport.disconnect.side_effect = arbitrary_exception
        stage.run_op(op)
        assert stage._pending_connection_op is None
 def op(self, mocker):
     return pipeline_ops_base.ReauthorizeConnectionOperation(
         callback=mocker.MagicMock())
示例#12
0
class TestMQTTTransportStageOnDisconnected(MQTTTransportStageTestBase):
    @pytest.mark.it(
        "Calls self.on_disconnected when the transport disconnected event fires"
    )
    @pytest.mark.parametrize(
        "cause",
        [
            pytest.param(None, id="No error cause"),
            pytest.param(SomeException(), id="With error cause"),
        ],
    )
    @pytest.mark.parametrize(
        "pending_connection_op",
        [
            pytest.param(None, id="No pending operation"),
            pytest.param(pipeline_ops_base.ConnectOperation(1),
                         id="Pending ConnectOperation"),
            pytest.param(
                pipeline_ops_base.ReauthorizeConnectionOperation(1),
                id="Pending ReauthorizeConnectionOperation",
            ),
            pytest.param(pipeline_ops_base.DisconnectOperation(1),
                         id="Pending DisconnectOperation"),
        ],
    )
    def test_disconnected_handler(self, stage, create_transport,
                                  pending_connection_op, cause):
        stage._pending_connection_op = pending_connection_op
        assert stage.previous.on_disconnected.call_count == 0
        stage.transport.on_mqtt_disconnected_handler(cause)
        assert stage.previous.on_disconnected.call_count == 1

    @pytest.mark.it(
        "Completes a pending DisconnectOperation with success when the transport disconnected event fires without an error cause"
    )
    def test_compltetes_pending_disconnect_op_when_no_error(
            self, mocker, stage, create_transport):
        op = pipeline_ops_base.DisconnectOperation(callback=mocker.MagicMock())
        mocker.spy(op, "complete")
        stage.run_op(op)
        assert op.complete.call_count == 0
        assert stage._pending_connection_op is op
        stage.transport.on_mqtt_disconnected_handler(None)
        assert op.complete.call_count == 1
        assert op.complete.call_args == mocker.call()
        assert stage._pending_connection_op is None

    @pytest.mark.it(
        "Completes a pending DisconnectOperation with success when the transport disconnected event fires with an error cause"
    )
    def test_completes_pending_disconnect_op_with_error(
            self, mocker, stage, create_transport, arbitrary_exception):
        op = pipeline_ops_base.DisconnectOperation(callback=mocker.MagicMock())
        mocker.spy(op, "complete")
        stage.run_op(op)
        assert op.complete.call_count == 0
        assert stage._pending_connection_op is op
        stage.transport.on_mqtt_disconnected_handler(arbitrary_exception)
        assert op.complete.call_count == 1
        assert op.complete.call_args == mocker.call()
        assert stage._pending_connection_op is None

    @pytest.mark.it(
        "Completes an unrelated pending operation when the transport disconnected event fires"
    )
    @pytest.mark.parametrize(
        "cause",
        [
            pytest.param(None, id="No error cause"),
            pytest.param(SomeException(), id="With error cause"),
        ],
    )
    @pytest.mark.parametrize(
        "pending_connection_op",
        [
            pytest.param(pipeline_ops_base.ConnectOperation(None),
                         id="Pending ConnectOperation"),
            pytest.param(
                pipeline_ops_base.ReauthorizeConnectionOperation(None),
                id="Pending ReauthorizeConnectionOperation",
            ),
        ],
    )
    def test_completes_unrelated_op(self, mocker, stage, create_transport,
                                    pending_connection_op, cause):
        pending_connection_op.completed = False
        mocker.spy(pending_connection_op, "complete")
        stage._pending_connection_op = pending_connection_op
        stage.transport.on_mqtt_disconnected_handler(cause)
        assert stage._pending_connection_op is None
        assert pending_connection_op.complete.call_count == 1
        if cause:
            assert pending_connection_op.complete.call_args[1][
                "error"] == cause
        else:
            assert (type(pending_connection_op.complete.call_args[1]["error"])
                    == transport_exceptions.ConnectionDroppedError)

    @pytest.mark.it(
        "Triggers the unhandled exception handler (with ConnectionDroppedError) when the disconnect is unexpected"
    )
    @pytest.mark.parametrize(
        "cause",
        [
            pytest.param(None, id="No error cause"),
            pytest.param(SomeException(), id="With error cause"),
        ],
    )
    def test_unexpected_disconnect(self, mocker, stage, create_transport,
                                   cause):
        # A disconnect is unexpected when there is no pending operation, or a pending, non-Disconnect operation
        mock_handler = mocker.patch.object(handle_exceptions,
                                           "handle_background_exception")
        stage.transport.on_mqtt_disconnected_handler(cause)
        assert mock_handler.call_count == 1
        assert isinstance(mock_handler.call_args[0][0],
                          transport_exceptions.ConnectionDroppedError)
        assert mock_handler.call_args[0][0].__cause__ is cause
示例#13
0
class TestMQTTTransportStageOnConnectionFailure(MQTTTransportStageTestBase):
    @pytest.mark.it(
        "Does not call self.on_connected when the transport connection failure event fires"
    )
    @pytest.mark.parametrize(
        "pending_connection_op",
        [
            pytest.param(None, id="No pending operation"),
            pytest.param(pipeline_ops_base.ConnectOperation(1),
                         id="Pending ConnectOperation"),
            pytest.param(
                pipeline_ops_base.ReauthorizeConnectionOperation(1),
                id="Pending ReauthorizeConnectionOperation",
            ),
            pytest.param(pipeline_ops_base.DisconnectOperation(1),
                         id="Pending DisconnectOperation"),
        ],
    )
    def test_does_not_call_connected_handler(self, stage, create_transport,
                                             arbitrary_exception,
                                             pending_connection_op):
        # This test is testing negative space - something the function does NOT do - rather than something it does
        stage._pending_connection_op = pending_connection_op
        assert stage.previous.on_connected.call_count == 0
        stage.transport.on_mqtt_connection_failure_handler(arbitrary_exception)
        assert stage.previous.on_connected.call_count == 0

    @pytest.mark.it(
        "Fails a pending ConnectOperation if the connection failure event fires"
    )
    def test_fails_pending_connect_op(self, mocker, stage, create_transport,
                                      arbitrary_exception):
        op = pipeline_ops_base.ConnectOperation(callback=mocker.MagicMock())
        mocker.spy(op, "complete")
        stage.run_op(op)
        assert op.complete.call_count == 0
        assert stage._pending_connection_op is op
        stage.transport.on_mqtt_connection_failure_handler(arbitrary_exception)
        assert op.complete.call_count == 1
        assert op.complete.call_args == mocker.call(error=arbitrary_exception)
        assert stage._pending_connection_op is None

    @pytest.mark.it(
        "Fails a pending ReauthorizeConnectionOperation if the connection failure event fires"
    )
    def test_fails_pending_reauthorize_connection_op(self, mocker, stage,
                                                     create_transport,
                                                     arbitrary_exception):
        op = pipeline_ops_base.ReauthorizeConnectionOperation(
            callback=mocker.MagicMock())
        mocker.spy(op, "complete")
        stage.run_op(op)
        assert op.complete.call_count == 0
        assert stage._pending_connection_op is op
        stage.transport.on_mqtt_connection_failure_handler(arbitrary_exception)
        assert op.complete.call_count == 1
        assert op.complete.call_args == mocker.call(error=arbitrary_exception)
        assert stage._pending_connection_op is None

    @pytest.mark.it(
        "Ignores a pending DisconnectOperation if the connection failure event fires"
    )
    def test_ignores_pending_disconnect_op(self, mocker, stage,
                                           create_transport,
                                           arbitrary_exception):
        op = pipeline_ops_base.DisconnectOperation(callback=mocker.MagicMock())
        mocker.spy(op, "complete")
        stage.run_op(op)
        assert op.complete.call_count == 0
        assert stage._pending_connection_op is op
        stage.transport.on_mqtt_connection_failure_handler(arbitrary_exception)
        # Assert nothing changed about the operation
        assert op.complete.call_count == 0
        assert stage._pending_connection_op is op

    @pytest.mark.it(
        "Triggers the unhandled exception handler (with error cause) when the connection failure is unexpected"
    )
    @pytest.mark.parametrize(
        "pending_connection_op",
        [
            pytest.param(None, id="No pending operation"),
            pytest.param(pipeline_ops_base.DisconnectOperation(1),
                         id="Pending DisconnectOperation"),
        ],
    )
    def test_unexpected_connection_failure(self, mocker, stage,
                                           create_transport,
                                           arbitrary_exception,
                                           pending_connection_op):
        # A connection failure is unexpected if there is not a pending Connect/ReauthorizeConnection operation
        # i.e. "Why did we get a connection failure? We weren't even trying to connect!"
        mock_handler = mocker.patch.object(handle_exceptions,
                                           "handle_background_exception")
        stage._pending_connection_operation = pending_connection_op
        stage.transport.on_mqtt_connection_failure_handler(arbitrary_exception)
        assert mock_handler.call_count == 1
        assert mock_handler.call_args[0][0] is arbitrary_exception
示例#14
0
class TestMQTTTransportStageOnConnected(MQTTTransportStageTestBase):
    @pytest.mark.it(
        "Calls self.on_connected when the transport connected event fires")
    @pytest.mark.parametrize(
        "pending_connection_op",
        [
            pytest.param(None, id="No pending operation"),
            pytest.param(pipeline_ops_base.ConnectOperation(1),
                         id="Pending ConnectOperation"),
            pytest.param(
                pipeline_ops_base.ReauthorizeConnectionOperation(1),
                id="Pending ReauthorizeConnectionOperation",
            ),
            pytest.param(pipeline_ops_base.DisconnectOperation(1),
                         id="Pending DisconnectOperation"),
        ],
    )
    def test_connected_handler(self, stage, create_transport,
                               pending_connection_op):
        stage._pending_connection_op = pending_connection_op
        assert stage.previous.on_connected.call_count == 0
        stage.transport.on_mqtt_connected_handler()
        assert stage.previous.on_connected.call_count == 1

    @pytest.mark.it(
        "Completes a pending ConnectOperation with success when the transport connected event fires"
    )
    def test_completes_pending_connect_op(self, mocker, stage,
                                          create_transport):
        op = pipeline_ops_base.ConnectOperation(callback=mocker.MagicMock())
        mocker.spy(op, "complete")
        stage.run_op(op)

        assert op.complete.call_count == 0
        assert stage._pending_connection_op is op

        stage.transport.on_mqtt_connected_handler()

        assert op.complete.call_count == 1
        assert op.complete.call_args == mocker.call()
        assert stage._pending_connection_op is None

    @pytest.mark.it(
        "Completes a pending ReauthorizeConnectionOperation with success when the transport connected event fires"
    )
    def test_completes_pending_reauthorize_connection_op(
            self, mocker, stage, create_transport):
        op = pipeline_ops_base.ReauthorizeConnectionOperation(
            callback=mocker.MagicMock())
        mocker.spy(op, "complete")
        stage.run_op(op)

        assert op.complete.call_count == 0
        assert stage._pending_connection_op is op

        stage.transport.on_mqtt_connected_handler()

        assert op.complete.call_count == 1
        assert op.complete.call_args == mocker.call()
        assert stage._pending_connection_op is None

    @pytest.mark.it(
        "Ignores a pending DisconnectOperation when the transport connected event fires"
    )
    def test_ignores_pending_disconnect_op(self, mocker, stage,
                                           create_transport):
        op = pipeline_ops_base.DisconnectOperation(callback=mocker.MagicMock())
        mocker.spy(op, "complete")
        stage.run_op(op)

        assert op.complete.call_count == 0
        assert stage._pending_connection_op is op

        stage.transport.on_mqtt_connected_handler()

        # handler did NOT trigger a completion
        assert op.complete.call_count == 0
        assert stage._pending_connection_op is op
示例#15
0
def op_reauthorize_connection(mocker):
    op = pipeline_ops_base.ReauthorizeConnectionOperation(
        callback=mocker.MagicMock())
    mocker.spy(op, "complete")
    return op