-2

I have a Netty TCP Server with Spring Boot 2.3.1 with the following handler :

@Slf4j
@Component
@RequiredArgsConstructor
@ChannelHandler.Sharable
public class QrReaderProcessingHandler extends ChannelInboundHandlerAdapter {

    private final CarParkPermissionService permissionService;
    private final Gson gson = new Gson();

    private String remoteAddress;

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.fireChannelActive();

        remoteAddress = ctx.channel().remoteAddress().toString();
        if (log.isDebugEnabled()) {
            log.debug(remoteAddress);
        }
        ctx.writeAndFlush("Your remote address is " + remoteAddress + ".\r\n");
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        log.info("CLIENT_IP: {}", remoteAddress);

        String stringMsg = (String) msg;
        log.info("CLIENT_REQUEST: {}", stringMsg);

        String lowerCaseMsg = stringMsg.toLowerCase();

        if (RequestType.HEARTBEAT.containsName(lowerCaseMsg)) {
            HeartbeatRequest heartbeatRequest = gson.fromJson(stringMsg, HeartbeatRequest.class);
            log.debug("heartbeat request: {}", heartbeatRequest);

            HeartbeatResponse response = HeartbeatResponse.builder()
                .responseCode("ok")
                .build();
            ctx.writeAndFlush(response + "\n\r");
        }
    }

Request DTO:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HeartbeatRequest {
    private String messageID;
}

Response DTO:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class HeartbeatResponse {
    private String responseCode;
}

Logic is quite simple. Only I have to know the IP address of the client.

I need to test it as well.

I have been looking for many resources for testing handlers for Netty, like

However, it didn't work for me.

For EmbeddedChannel I have following error - Your remote address is embedded.

Here is code:

@ActiveProfiles("test")
@RunWith(MockitoJUnitRunner.class)
public class ProcessingHandlerTest_Embedded {

    @Mock
    private PermissionService permissionService;
    private EmbeddedChannel embeddedChannel;
    private final Gson gson = new Gson();

    private ProcessingHandler processingHandler;


    @Before
    public void setUp() {
        processingHandler = new ProcessingHandler(permissionService);
        embeddedChannel = new EmbeddedChannel(processingHandler);
    }

    @Test
    public void testHeartbeatMessage() {
        // given
        HeartbeatRequest heartbeatMessage = HeartbeatRequest.builder()
                .messageID("heartbeat")
                .build();

        HeartbeatResponse response = HeartbeatResponse.builder()
                .responseCode("ok")
                .build();
        String request = gson.toJson(heartbeatMessage).concat("\r\n");
        String expected = gson.toJson(response).concat("\r\n");

        // when
        embeddedChannel.writeInbound(request);

        // then
        Queue<Object> outboundMessages = embeddedChannel.outboundMessages();
        assertEquals(expected, outboundMessages.poll());
    }
}

Output:

22:21:29.062 [main] INFO handler.ProcessingHandler - CLIENT_IP: embedded
22:21:29.062 [main] INFO handler.ProcessingHandler - CLIENT_REQUEST: {"messageID":"heartbeat"}

22:21:29.067 [main] DEBUG handler.ProcessingHandler - heartbeat request: HeartbeatRequest(messageID=heartbeat)

org.junit.ComparisonFailure: 
<Click to see difference>

enter image description here

However, I don't know how to do exact testing for such a case.

Here is a snippet from configuration:

@Bean
@SneakyThrows
public InetSocketAddress tcpSocketAddress() {
    // for now, hostname is: localhost/127.0.0.1:9090
    return new InetSocketAddress("localhost", nettyProperties.getTcpPort());

    // for real client devices: A05264/172.28.1.162:9090
    // return new InetSocketAddress(InetAddress.getLocalHost(), nettyProperties.getTcpPort());
}

@Component
@RequiredArgsConstructor
public class QrReaderChannelInitializer extends ChannelInitializer<SocketChannel> {

    private final StringEncoder stringEncoder = new StringEncoder();
    private final StringDecoder stringDecoder = new StringDecoder();

    private final QrReaderProcessingHandler readerServerHandler;
    private final NettyProperties nettyProperties;

    @Override
    protected void initChannel(SocketChannel socketChannel) {
        ChannelPipeline pipeline = socketChannel.pipeline();

        // Add the text line codec combination first
        pipeline.addLast(new DelimiterBasedFrameDecoder(1024 * 1024, Delimiters.lineDelimiter()));

        pipeline.addLast(new ReadTimeoutHandler(nettyProperties.getClientTimeout()));
        pipeline.addLast(stringDecoder);
        pipeline.addLast(stringEncoder);
        pipeline.addLast(readerServerHandler);
    }
}

How to test handler with IP address of a client?

catch23
  • 17,519
  • 42
  • 144
  • 217

1 Answers1

1

Two things that could help:

  1. Do not annotate with @ChannelHandler.Sharable if your handler is NOT sharable. This can be misleading. Remove unnecessary state from handlers. In your case you should remove the remoteAddress member variable and ensure that Gson and CarParkPermissionService can be reused and are thread-safe.

  2. "Your remote address is embedded" is NOT an error. It actually is the message written by your handler onto the outbound channel (cf. your channelActive() method)

So it looks like it could work.

EDIT

Following your comments here are some clarifications regarding the second point. I mean that:

  • your code making use of EmbeddedChannel is almost correct. There is just a misunderstanding on the expected results (assert).

To make the unit test successful, you just have either:

  • to comment this line in channelActive(): ctx.writeAndFlush("Your remote ...")
  • or to poll the second message from Queue<Object> outboundMessages in testHeartbeatMessage()

Indeed, when you do this:

// when
embeddedChannel.writeInbound(request);

(1) You actually open the channel once, which fires a channelActive() event. You don't have a log in it but we see that the variable remoteAddress is not null afterwards, meaning that it was assigned in the channelActive() method.

(2) At the end of the channelActive() method, you eventually already send back a message by writing on the channel pipeline, as seen at this line:

ctx.writeAndFlush("Your remote address is " + remoteAddress + ".\r\n");
// In fact, this is the message you see in your failed assertion.

(3) Then the message written by embeddedChannel.writeInbound(request) is received and can be read, which fires a channelRead() event. This time, we see this in your log output:

22:21:29.062 [main] INFO handler.ProcessingHandler - CLIENT_IP: embedded
22:21:29.062 [main] INFO handler.ProcessingHandler - CLIENT_REQUEST: {"messageID":"heartbeat"}
22:21:29.067 [main] DEBUG handler.ProcessingHandler - heartbeat request: HeartbeatRequest(messageID=heartbeat)

(4) At the end of channelRead(ChannelHandlerContext ctx, Object msg), you will then send a second message (the expected one):

HeartbeatResponse response = HeartbeatResponse.builder()
     .responseCode("ok")
     .build();
ctx.writeAndFlush(response + "\n\r");

Therefore, with the following code of your unit test...

Queue<Object> outboundMessages = embeddedChannel.outboundMessages();
assertEquals(expected, outboundMessages.poll());

... you should be able to poll() two messages:

  • "Your remote address is embedded"
  • "{ResponseCode":"ok"}

Does it make sense for you?

bsaverino
  • 1,221
  • 9
  • 14
  • already removed `remoteAddress` from code, it was presented only here for question. Have to check if Gson is thread safe. I suppose that sharable will indicated that many clients could work with this handler at the same time. – catch23 Aug 15 '20 at 03:39
  • Gson is thread-safe. Even after removing `@ChannelHandler.Sharable` the result is the same for `EmbeddedChannel` case. – catch23 Aug 15 '20 at 08:38
  • `@ChannelHandler.Sharable` is only a hint for developers (as far as I know and Javadoc states) so it will not change the behavior. It helps in avoiding mistakes when creating this handler and adding it to the pipelines. The second point is more interesting, what about it? Did you notice that you actually receive the expected result? – bsaverino Aug 15 '20 at 13:04
  • unfortunately no. Result is the same - `your remote address is embedded` – catch23 Aug 15 '20 at 13:13
  • Yes but why is it an error? This is just your message sent on the wire. Cf. ctx.writeAndFlush("...") in your channelActive() méthod. Its normal to receive it as its sent after connect. Then just comment the line? – bsaverino Aug 15 '20 at 13:31
  • What exactly do you mean? My handler checks for request message and sent back response. Thus, response message should be expected. – catch23 Aug 15 '20 at 13:35
  • Ok I added more details for your understanding. Tell me if it helps. – bsaverino Aug 15 '20 at 21:48
  • could you help me to understand one more thing about `pipeline.addLast(new DelimiterBasedFrameDecoder(1024 * 1024, Delimiters.lineDelimiter()));` -> what exactly it does? How is it related to `\r\n`? – catch23 Aug 15 '20 at 22:04
  • Happy it could help. About the DelimiterBasedFrameDecoder I believe the Netty Javadoc describes it quite well: https://netty.io/4.0/api/io/netty/handler/codec/DelimiterBasedFrameDecoder.html – bsaverino Aug 15 '20 at 22:33