async def test_when_dispatching_stream_iterators(): """ When we're dealing with iterators, the first message should result in a StreamingIterator being returned to the caller; subsequent messages should push new events to the caller, and the final message should terminate iteration. """ conversation = IterStreamEvents("my-stream") first_msg = read_stream_events_completed( conversation.conversation_id, "my-stream", [NewEvent("event", data={"x": 1})]) second_msg = read_stream_events_completed( conversation.conversation_id, "my-stream", [NewEvent("event", data={"x": 2}), NewEvent("event", data={"x": 3})], ) final_msg = read_stream_events_completed( conversation.conversation_id, "my-stream", [NewEvent("event", data={"x": 4})], end_of_stream=True, ) dispatcher = MessageDispatcher() output_queue = TeeQueue() future = await dispatcher.start_conversation(conversation) # The first message should result in an iterator being returned to the caller # with one event. The conversation should still be active. await dispatcher.dispatch(first_msg, output_queue) iterator = await asyncio.wait_for(future, 1) e = await anext(iterator) assert e.event.json()["x"] == 1 assert dispatcher.has_conversation(conversation.conversation_id) # The second message should result in two events on the iterator. # The conversation should still be active. await dispatcher.dispatch(second_msg, output_queue) e = await anext(iterator) assert e.event.json()["x"] == 2 e = await anext(iterator) assert e.event.json()["x"] == 3 assert dispatcher.has_conversation(conversation.conversation_id) # The final message should result in one event and the iterator terminating await dispatcher.dispatch(final_msg, output_queue) [e] = [e async for e in iterator] assert e.event.json()["x"] == 4 assert not dispatcher.has_conversation(conversation.conversation_id)
async def test_when_a_stream_iterator_fails_midway(): """ Sometimes bad things happen. What happens if we're iterating a stream and some anarchist decides to delete it from under our feet? Well, I guess we should return an error to the caller and stop the iterator. """ dispatcher = MessageDispatcher() output_queue = TeeQueue() conversation = IterStreamEvents("my-stream") first_msg = read_stream_events_completed( conversation.conversation_id, "my-stream", [NewEvent("event", data={"x": 1})]) second_msg = read_stream_events_failure(conversation.conversation_id, ReadStreamResult.StreamDeleted) future = await dispatcher.start_conversation(conversation) await dispatcher.dispatch(first_msg, output_queue) await dispatcher.dispatch(second_msg, output_queue) iterator = await asyncio.wait_for(future, 1) event = await anext(iterator) assert event.event.json()["x"] == 1 with pytest.raises(StreamDeleted): await anext(iterator) assert not dispatcher.has_conversation(conversation.conversation_id)
async def test_when_enqueuing_a_conversation(): """ When we enqueue a conversation, we should add the first message of the conversation to the outbound queue. """ conversation = Ping() output = TeeQueue() dispatcher = MessageDispatcher() await dispatcher.write_to(output) await dispatcher.start_conversation(conversation) msg = await output.get() assert msg.command == TcpCommand.Ping assert dispatcher.has_conversation(conversation.conversation_id)
async def test_when_a_stream_iterator_fails_at_the_first_hurdle(): """ If we receive an error in reply to the first message in an iterator conversation, then we ought to raise the error instead of returning an iterator. """ dispatcher = MessageDispatcher() conversation = IterStreamEvents("my-stream") first_msg = read_stream_events_failure(conversation.conversation_id, ReadStreamResult.StreamDeleted) future = await dispatcher.start_conversation(conversation) await dispatcher.dispatch(first_msg, None) with pytest.raises(StreamDeleted): await asyncio.wait_for(future, 1) assert not dispatcher.has_conversation(conversation.conversation_id)
async def test_when_the_conversation_raises_an_error(): """ If the conversation raises an error, that error should be returned to the caller. """ out_queue = TeeQueue() dispatcher = MessageDispatcher() conversation = Ping() future = await dispatcher.start_conversation(conversation) await dispatcher.dispatch( InboundMessage(conversation.conversation_id, TcpCommand.NotAuthenticated, bytes()), out_queue, ) with pytest.raises(NotAuthenticated): await asyncio.wait_for(future, 1) assert not dispatcher.has_conversation(conversation.conversation_id)
async def test_when_the_conversation_receives_an_unexpected_response(): """ If the conversation receives an unhandled response, an error should be returned to the caller. This is a separate code path for now, but should probably be cleaned up in the Conversation. Maybe use a decorator? """ out_queue = TeeQueue() dispatcher = MessageDispatcher() conversation = Ping() future = await dispatcher.start_conversation(conversation) await dispatcher.dispatch( InboundMessage(conversation.conversation_id, TcpCommand.WriteEventsCompleted, bytes()), out_queue, ) with pytest.raises(PayloadUnreadable): await asyncio.wait_for(future, 1) assert not dispatcher.has_conversation(conversation.conversation_id)