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))
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
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())
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 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
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
def op_reauthorize_connection(mocker): op = pipeline_ops_base.ReauthorizeConnectionOperation( callback=mocker.MagicMock()) mocker.spy(op, "complete") return op