package org.jooby.internal.netty; import static io.netty.channel.ChannelFutureListener.CLOSE; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expectLastCall; import static org.easymock.EasyMock.isA; import static org.junit.Assert.assertEquals; import java.nio.ByteBuffer; import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.function.BiConsumer; import java.util.function.Consumer; import org.jooby.WebSocket; import org.jooby.test.MockUnit; import org.jooby.test.MockUnit.Block; import org.junit.Test; import org.junit.runner.RunWith; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelConfig; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; import io.netty.util.Attribute; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; @RunWith(PowerMockRunner.class) @PrepareForTest({NettyWebSocket.class, CloseWebSocketFrame.class, Unpooled.class, CountDownLatch.class, Thread.class }) public class NettyWebSocketTest { private Block channel = unit -> { Channel channel = unit.mock(Channel.class); unit.registerMock(Channel.class, channel); ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class); expect(ctx.channel()).andReturn(channel).times(1, 2); }; private MockUnit.Block close = unit -> { Channel ch = unit.get(Channel.class); CloseWebSocketFrame frame = unit.mockConstructor( CloseWebSocketFrame.class, new Class[]{int.class, String.class }, 1001, "normal"); ChannelFuture future = unit.mock(ChannelFuture.class); expect(future.addListener(CLOSE)).andReturn(future); WebSocketServerHandshaker handshaker = unit.get(WebSocketServerHandshaker.class); expect(handshaker.close(ch, frame)).andReturn(future); }; @SuppressWarnings("unchecked") @Test public void close() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class) .expect(channel) .expect(close) .expect(unit -> { Attribute<NettyWebSocket> attr = unit.mock(Attribute.class); attr.set(null); Channel ctx = unit.get(Channel.class); expect(ctx.attr(NettyWebSocket.KEY)).andReturn(attr); }) .run(unit -> { new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)).close(1001, "normal"); }); } @SuppressWarnings("unchecked") @Test public void closeNoAttr() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class) .expect(channel) .expect(close) .expect(unit -> { Channel ctx = unit.get(Channel.class); expect(ctx.attr(NettyWebSocket.KEY)).andReturn(null); }) .run(unit -> { new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)).close(1001, "normal"); }); } @SuppressWarnings("unchecked") @Test public void resume() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class) .expect(unit -> { ChannelConfig chconf = unit.mock(ChannelConfig.class); expect(chconf.isAutoRead()).andReturn(false); expect(chconf.setAutoRead(true)).andReturn(chconf); Channel ch = unit.mock(Channel.class); expect(ch.config()).andReturn(chconf); ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class); expect(ctx.channel()).andReturn(ch); }) .run(unit -> { new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)).resume(); }); } @SuppressWarnings("unchecked") @Test public void resumeIgnored() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class) .expect(unit -> { ChannelConfig chconf = unit.mock(ChannelConfig.class); expect(chconf.isAutoRead()).andReturn(true); Channel ch = unit.mock(Channel.class); expect(ch.config()).andReturn(chconf); ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class); expect(ctx.channel()).andReturn(ch); }) .run(unit -> { new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)).resume(); }); } @SuppressWarnings("unchecked") @Test public void pause() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class) .expect(unit -> { ChannelConfig chconf = unit.mock(ChannelConfig.class); expect(chconf.isAutoRead()).andReturn(true); expect(chconf.setAutoRead(false)).andReturn(chconf); Channel ch = unit.mock(Channel.class); expect(ch.config()).andReturn(chconf); ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class); expect(ctx.channel()).andReturn(ch); }) .run(unit -> { new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)).pause(); }); } @SuppressWarnings("unchecked") @Test public void pauseIgnored() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class) .expect(unit -> { ChannelConfig chconf = unit.mock(ChannelConfig.class); expect(chconf.isAutoRead()).andReturn(false); Channel ch = unit.mock(Channel.class); expect(ch.config()).andReturn(chconf); ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class); expect(ctx.channel()).andReturn(ch); }) .run(unit -> { new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)).pause(); }); } @SuppressWarnings("unchecked") @Test public void terminate() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class, BiConsumer.class) .expect(unit -> { BiConsumer<Integer, Optional<String>> callback = unit.get(BiConsumer.class); callback.accept(1006, Optional.of("Harsh disconnect")); ChannelFuture future = unit.mock(ChannelFuture.class); expect(future.addListener(CLOSE)).andReturn(future); ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class); expect(ctx.disconnect()).andReturn(future); }) .run(unit -> { NettyWebSocket ws = new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)); ws.onCloseMessage(unit.get(BiConsumer.class)); ws.terminate(); }); } @SuppressWarnings({"unchecked", "rawtypes" }) @Test public void sendBytes() throws Exception { ByteBuffer buffer = ByteBuffer.wrap(new byte[]{'a', 'b', 'c' }); new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class, WebSocket.SuccessCallback.class, WebSocket.OnError.class, Future.class) .expect(unit -> { ByteBuf byteBuf = unit.mock(ByteBuf.class); unit.mockStatic(Unpooled.class); expect(Unpooled.wrappedBuffer(buffer)).andReturn(byteBuf); ChannelFuture future = unit.mock(ChannelFuture.class); expect(future.addListener(unit.capture(GenericFutureListener.class))) .andReturn(future); BinaryWebSocketFrame frame = unit.mockConstructor(BinaryWebSocketFrame.class, new Class[]{ByteBuf.class }, byteBuf); Channel ch = unit.mock(Channel.class); expect(ch.writeAndFlush(frame)).andReturn(future); ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class); expect(ctx.channel()).andReturn(ch); }) .expect(unit -> { Future future = unit.get(Future.class); expect(future.isSuccess()).andReturn(true); WebSocket.SuccessCallback success = unit.get(WebSocket.SuccessCallback.class); success.invoke(); }) .run( unit -> { NettyWebSocket ws = new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)); ws.sendBytes(buffer, unit.get(WebSocket.SuccessCallback.class), unit.get(WebSocket.OnError.class)); }, unit -> { GenericFutureListener listener = unit.captured(GenericFutureListener.class) .iterator().next(); listener.operationComplete(unit.get(Future.class)); }); } @SuppressWarnings({"unchecked", "rawtypes" }) @Test public void sendString() throws Exception { String data = "abc"; new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class, WebSocket.SuccessCallback.class, WebSocket.OnError.class, Future.class) .expect(unit -> { ChannelFuture future = unit.mock(ChannelFuture.class); expect(future.addListener(unit.capture(GenericFutureListener.class))) .andReturn(future); TextWebSocketFrame frame = unit.mockConstructor(TextWebSocketFrame.class, new Class[]{String.class }, data); Channel ch = unit.mock(Channel.class); expect(ch.writeAndFlush(frame)).andReturn(future); ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class); expect(ctx.channel()).andReturn(ch); }) .expect(unit -> { Future future = unit.get(Future.class); expect(future.isSuccess()).andReturn(true); WebSocket.SuccessCallback success = unit.get(WebSocket.SuccessCallback.class); success.invoke(); }) .run( unit -> { NettyWebSocket ws = new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)); ws.sendText(data, unit.get(WebSocket.SuccessCallback.class), unit.get(WebSocket.OnError.class)); }, unit -> { GenericFutureListener listener = unit.captured(GenericFutureListener.class) .iterator().next(); listener.operationComplete(unit.get(Future.class)); }); } @SuppressWarnings({"unchecked", "rawtypes" }) @Test public void sendBytesFailure() throws Exception { ByteBuffer buffer = ByteBuffer.wrap(new byte[]{'a', 'b', 'c' }); new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class, WebSocket.SuccessCallback.class, WebSocket.OnError.class, Future.class) .expect(unit -> { ByteBuf byteBuf = unit.mock(ByteBuf.class); unit.mockStatic(Unpooled.class); expect(Unpooled.wrappedBuffer(buffer)).andReturn(byteBuf); ChannelFuture future = unit.mock(ChannelFuture.class); expect(future.addListener(unit.capture(GenericFutureListener.class))) .andReturn(future); BinaryWebSocketFrame frame = unit.mockConstructor(BinaryWebSocketFrame.class, new Class[]{ByteBuf.class }, byteBuf); Channel ch = unit.mock(Channel.class); expect(ch.writeAndFlush(frame)).andReturn(future); ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class); expect(ctx.channel()).andReturn(ch); }) .expect(unit -> { Throwable cause = new NullPointerException(); Future future = unit.get(Future.class); expect(future.isSuccess()).andReturn(false); expect(future.cause()).andReturn(cause); WebSocket.OnError err = unit.get(WebSocket.OnError.class); err.onError(cause); }) .run( unit -> { NettyWebSocket ws = new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)); ws.sendBytes(buffer, unit.get(WebSocket.SuccessCallback.class), unit.get(WebSocket.OnError.class)); }, unit -> { GenericFutureListener listener = unit.captured(GenericFutureListener.class) .iterator().next(); listener.operationComplete(unit.get(Future.class)); }); } @SuppressWarnings({"unchecked", "rawtypes" }) @Test public void sendStringFailure() throws Exception { String data = "abc"; new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class, WebSocket.SuccessCallback.class, WebSocket.OnError.class, Future.class) .expect(unit -> { ChannelFuture future = unit.mock(ChannelFuture.class); expect(future.addListener(unit.capture(GenericFutureListener.class))) .andReturn(future); TextWebSocketFrame frame = unit.mockConstructor(TextWebSocketFrame.class, new Class[]{String.class }, data); Channel ch = unit.mock(Channel.class); expect(ch.writeAndFlush(frame)).andReturn(future); ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class); expect(ctx.channel()).andReturn(ch); }) .expect(unit -> { Throwable cause = new NullPointerException(); Future future = unit.get(Future.class); expect(future.isSuccess()).andReturn(false); expect(future.cause()).andReturn(cause); WebSocket.OnError err = unit.get(WebSocket.OnError.class); err.onError(cause); }) .run( unit -> { NettyWebSocket ws = new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)); ws.sendText(data, unit.get(WebSocket.SuccessCallback.class), unit.get(WebSocket.OnError.class)); }, unit -> { GenericFutureListener listener = unit.captured(GenericFutureListener.class) .iterator().next(); listener.operationComplete(unit.get(Future.class)); }); } @SuppressWarnings("unchecked") @Test public void isOpen() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class) .expect(unit -> { Channel ch = unit.mock(Channel.class); expect(ch.isOpen()).andReturn(true); ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class); expect(ctx.channel()).andReturn(ch); }) .run(unit -> { assertEquals(true, new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)).isOpen()); }); } @SuppressWarnings("unchecked") @Test public void isNoOpen() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class) .expect(unit -> { Channel ch = unit.mock(Channel.class); expect(ch.isOpen()).andReturn(false); ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class); expect(ctx.channel()).andReturn(ch); }) .run(unit -> { assertEquals(false, new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)).isOpen()); }); } @SuppressWarnings("unchecked") @Test public void connect() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class, CountDownLatch.class, Runnable.class) .expect(unit -> { CountDownLatch ready = unit.mockConstructor(CountDownLatch.class, new Class[]{int.class }, 1); ready.countDown(); unit.get(Runnable.class).run(); }) .run(unit -> { NettyWebSocket ws = new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)); ws.onConnect(unit.get(Runnable.class)); ws.connect(); }); } @SuppressWarnings("unchecked") @Test public void hankshake() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, CountDownLatch.class, Consumer.class) .expect(unit -> { unit.get(Consumer.class).accept(isA(NettyWebSocket.class)); }) .run(unit -> { NettyWebSocket ws = new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)); ws.hankshake(); }); } @SuppressWarnings("unchecked") @Test public void handleTextFrame() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class, TextWebSocketFrame.class) .expect(unit -> { CountDownLatch ready = unit.mockConstructor(CountDownLatch.class, new Class[]{int.class }, 1); ready.await(); TextWebSocketFrame frame = unit.get(TextWebSocketFrame.class); expect(frame.text()).andReturn("text"); Consumer<String> callback = unit.get(Consumer.class); callback.accept("text"); }) .run(unit -> { NettyWebSocket ws = new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)); ws.onTextMessage(unit.get(Consumer.class)); ws.handle(unit.get(TextWebSocketFrame.class)); }); } @SuppressWarnings("unchecked") @Test public void handleBinaryFrame() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class, BinaryWebSocketFrame.class) .expect(unit -> { CountDownLatch ready = unit.mockConstructor(CountDownLatch.class, new Class[]{int.class }, 1); ready.await(); ByteBuffer nioBuff = ByteBuffer.wrap(new byte[0]); ByteBuf buff = unit.mock(ByteBuf.class); expect(buff.nioBuffer()).andReturn(nioBuff); BinaryWebSocketFrame frame = unit.get(BinaryWebSocketFrame.class); expect(frame.content()).andReturn(buff); Consumer<ByteBuffer> callback = unit.get(Consumer.class); callback.accept(nioBuff); }) .run(unit -> { NettyWebSocket ws = new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)); ws.onBinaryMessage(unit.get(Consumer.class)); ws.handle(unit.get(BinaryWebSocketFrame.class)); }); } @SuppressWarnings("unchecked") @Test public void handleInterruped() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class, WebSocketFrame.class) .expect(unit -> { CountDownLatch ready = unit.mockConstructor(CountDownLatch.class, new Class[]{int.class }, 1); ready.await(); expectLastCall().andThrow(new InterruptedException("intentional err")); Thread thread = unit.mock(Thread.class); thread.interrupt(); unit.mockStatic(Thread.class); expect(Thread.currentThread()).andReturn(thread); }) .run(unit -> { NettyWebSocket ws = new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)); ws.handle(unit.get(WebSocketFrame.class)); }); } @SuppressWarnings("unchecked") @Test public void handleCloseFrame() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class, CloseWebSocketFrame.class, BiConsumer.class) .expect(unit -> { CountDownLatch ready = unit.mockConstructor(CountDownLatch.class, new Class[]{int.class }, 1); ready.await(); CloseWebSocketFrame retain = unit.get(CloseWebSocketFrame.class); expect(retain.statusCode()).andReturn(-1); expect(retain.reasonText()).andReturn(null); CloseWebSocketFrame frame = unit.get(CloseWebSocketFrame.class); expect(frame.retain()).andReturn(retain); BiConsumer<Integer, Optional<String>> callback = unit.get(BiConsumer.class); callback.accept(1000, Optional.empty()); Channel ch = unit.mock(Channel.class); ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class); expect(ctx.channel()).andReturn(ch); ChannelFuture future = unit.mock(ChannelFuture.class); expect(future.addListener(CLOSE)).andReturn(future); WebSocketServerHandshaker handshaker = unit.get(WebSocketServerHandshaker.class); expect(handshaker.close(ch, retain)).andReturn(future); }) .run(unit -> { NettyWebSocket ws = new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)); ws.onCloseMessage(unit.get(BiConsumer.class)); ws.handle(unit.get(CloseWebSocketFrame.class)); }); } @SuppressWarnings("unchecked") @Test public void handleCloseWithStatusFrame() throws Exception { new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class, CloseWebSocketFrame.class, BiConsumer.class) .expect(unit -> { CountDownLatch ready = unit.mockConstructor(CountDownLatch.class, new Class[]{int.class }, 1); ready.await(); CloseWebSocketFrame retain = unit.get(CloseWebSocketFrame.class); expect(retain.statusCode()).andReturn(1001); expect(retain.reasonText()).andReturn("normal"); CloseWebSocketFrame frame = unit.get(CloseWebSocketFrame.class); expect(frame.retain()).andReturn(retain); BiConsumer<Integer, Optional<String>> callback = unit.get(BiConsumer.class); callback.accept(1001, Optional.of("normal")); Channel ch = unit.mock(Channel.class); ChannelHandlerContext ctx = unit.get(ChannelHandlerContext.class); expect(ctx.channel()).andReturn(ch); ChannelFuture future = unit.mock(ChannelFuture.class); expect(future.addListener(CLOSE)).andReturn(future); WebSocketServerHandshaker handshaker = unit.get(WebSocketServerHandshaker.class); expect(handshaker.close(ch, retain)).andReturn(future); }) .run(unit -> { NettyWebSocket ws = new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)); ws.onCloseMessage(unit.get(BiConsumer.class)); ws.handle(unit.get(CloseWebSocketFrame.class)); }); } @SuppressWarnings("unchecked") @Test public void handleException() throws Exception { Throwable cause = new NullPointerException("intentional err"); new MockUnit(ChannelHandlerContext.class, WebSocketServerHandshaker.class, Consumer.class) .expect(unit -> { CountDownLatch ready = unit.mockConstructor(CountDownLatch.class, new Class[]{int.class }, 1); ready.await(); Consumer<Throwable> callback = unit.get(Consumer.class); callback.accept(cause); }) .run(unit -> { NettyWebSocket ws = new NettyWebSocket( unit.get(ChannelHandlerContext.class), unit.get(WebSocketServerHandshaker.class), unit.get(Consumer.class)); ws.onErrorMessage(unit.get(Consumer.class)); ws.handle(cause); }); } }