def disconnect(self, callback): """ Disconnect from the service. Note that even if this fails for some reason, the client will be in a disconnected state. :param callback: callback which is called when the disconnection 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.ProtocolClientError` """ self._verify_running() logger.debug("Starting DisconnectOperation on the pipeline") def on_complete(op, error): callback(error=error) self._pipeline.run_op( pipeline_ops_base.DisconnectOperation(callback=on_complete))
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
def op(self, mocker): return pipeline_ops_base.DisconnectOperation( callback=mocker.MagicMock())
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 test_disconnect_while_disconnected(self, stage, mocker): op = pipeline_ops_base.DisconnectOperation(callback=mocker.MagicMock()) stage.pipeline_root.connected = False stage.run_op(op) assert_callback_succeeded(op=op)
class TestMQTTProviderOnDisconnected(object): @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.ReconnectOperation(1), id="Pending ReconnectOperation"), 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()) stage.run_op(op) assert op.callback.call_count == 0 assert stage._pending_connection_op is op stage.transport.on_mqtt_disconnected_handler(None) assert_callback_succeeded(op=op) 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()) stage.run_op(op) assert op.callback.call_count == 0 assert stage._pending_connection_op is op stage.transport.on_mqtt_disconnected_handler(arbitrary_exception) assert_callback_succeeded(op=op) assert stage._pending_connection_op is None @pytest.mark.it( "Ignores 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(1), id="Pending ConnectOperation"), pytest.param(pipeline_ops_base.ReconnectOperation(1), id="Pending ReconnectOperation"), ], ) def test_ignores_unrelated_op(self, mocker, stage, create_transport, pending_connection_op, cause): stage._pending_connection_op = pending_connection_op stage.transport.on_mqtt_disconnected_handler(cause) # The unrelated pending operation is STILL the pending connection op assert stage._pending_connection_op is pending_connection_op @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"), ], ) @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.ReconnectOperation(1), id="Pending ReconnectOperation"), ], ) def test_unexpected_disconnect(self, mocker, stage, create_transport, pending_connection_op, 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._pending_connection_op = pending_connection_op 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
class TestMQTTProviderOnConnectionFailure(object): @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.ReconnectOperation(1), id="Pending ReconnectOperation"), 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()) stage.run_op(op) assert op.callback.call_count == 0 assert stage._pending_connection_op is op stage.transport.on_mqtt_connection_failure_handler(arbitrary_exception) assert_callback_failed(op=op, error=arbitrary_exception) assert stage._pending_connection_op is None @pytest.mark.it( "Fails a pending ReconnectOperation if the connection failure event fires" ) def test_fails_pending_reconnect_op(self, mocker, stage, create_transport, arbitrary_exception): op = pipeline_ops_base.ReconnectOperation(callback=mocker.MagicMock()) stage.run_op(op) assert op.callback.call_count == 0 assert stage._pending_connection_op is op stage.transport.on_mqtt_connection_failure_handler(arbitrary_exception) assert_callback_failed(op=op, 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()) stage.run_op(op) assert op.callback.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.callback.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/Reconnect 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
class TestMQTTProviderOnConnected(object): @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.ReconnectOperation(1), id="Pending ReconnectOperation"), 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()) stage.run_op(op) assert op.callback.call_count == 0 assert stage._pending_connection_op is op stage.transport.on_mqtt_connected_handler() assert_callback_succeeded(op=op) assert stage._pending_connection_op is None @pytest.mark.it( "Completes a pending ReconnectOperation with success when the transport connected event fires" ) def test_completes_pending_reconnect_op(self, mocker, stage, create_transport): op = pipeline_ops_base.ReconnectOperation(callback=mocker.MagicMock()) stage.run_op(op) assert op.callback.call_count == 0 assert stage._pending_connection_op is op stage.transport.on_mqtt_connected_handler() assert_callback_succeeded(op=op) 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()) stage.run_op(op) assert op.callback.call_count == 0 assert stage._pending_connection_op is op stage.transport.on_mqtt_connected_handler() # handler did NOT trigger a callback assert op.callback.call_count == 0 assert stage._pending_connection_op is op
def op_disconnect(mocker): op = pipeline_ops_base.DisconnectOperation(callback=mocker.MagicMock()) mocker.spy(op, "complete") return op
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
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