How to Receive Response from Websocket Unit Test in Springboot - spring-boot

I am new to websockets and I am trying to write a unit test.
My unit test runs fine but it has following two issue
Idk why but it forces me to expect same object that is being sent as an input(i.e WebSocketRequestData) to the websocket instead of the actual response from the websocket which is WebSocketData
And it returns an empty object as result so it passes NotNull assertion.
Can anyone please clear out this confusion for me!
And also what is the right way to get response from the my websocket in unit test?
here is the code for my websocketTest Class
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ServerWebSocketTest {
#LocalServerPort
private Integer port;
static final String WEBSOCKET_TOPIC = "/user/locationrealtimedata/item" ;
BlockingQueue<WebSocketRequestData> blockingQueue;
WebSocketStompClient stompClient;
#BeforeEach
public void setup() {
blockingQueue = new LinkedBlockingDeque<>();
stompClient = new WebSocketStompClient(new SockJsClient(
asList(new WebSocketTransport(new StandardWebSocketClient()))));
stompClient.setMessageConverter(new MappingJackson2MessageConverter());
}
#Test
public void shouldReceiveAMessageFromTheServer() throws Exception {
StompSession session = stompClient
.connect(getWsPath(), new DefaultStompFrameHandler() {
})
.get(1, TimeUnit.SECONDS);
session.subscribe(WEBSOCKET_TOPIC, new DefaultStompFrameHandler());
WebSocketRequestData webSocketRequestData = new WebSocketRequestData();
webSocketRequestData.setUserId("usr-1");
webSocketRequestData.setAccountId("acc-1");
webSocketRequestData.setGroupId("grp-1");
session.send("/wsconn/start", webSocketRequestData);
WebSocketRequestData responseObj = blockingQueue.poll(15, TimeUnit.SECONDS);
Assertions.assertNotNull(responseObj);
}
class DefaultStompFrameHandler extends StompSessionHandlerAdapter{
#Override
public Type getPayloadType(StompHeaders stompHeaders) {
return WebSocketRequestData.class;
}
#Override
public void handleFrame(StompHeaders stompHeaders, Object o) {
blockingQueue.offer((WebSocketRequestData) o); // instead of **WebSocketData** it forces me to add casting for **WebSocketRequestData**
}
#Override
public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exception) {
exception.printStackTrace();
}
}
private String getWsPath() {
return String.format("ws://localhost:%d/location_services/locationrealtimedata", port);
}
}
Thanks in advance

You are not forced to use the same Java class for the input and response type.
The request type is what you use within session.send("/endpoint", payload); in your case that's WebSocketRequestData:
WebSocketRequestData webSocketRequestData = new WebSocketRequestData();
webSocketRequestData.setUserId("usr-1");
webSocketRequestData.setAccountId("acc-1");
webSocketRequestData.setGroupId("grp-1");
session.send("/wsconn/start", webSocketRequestData);
When it comes to consuming messages you specify the actual response type you expect when implementing StompFrameHandler and overriding getPayloadType.
So instead of implementing StompSessionHandlerAdapter, use the StompFrameHandler interface and implement it as the following:
class DefaultStompFrameHandler extends StompSessionHandlerAdapter{
#Override
public Type getPayloadType(StompHeaders stompHeaders) {
return WebSocketData.class; // or any other class your expect
}
#Override
public void handleFrame(StompHeaders stompHeaders, Object o) {
blockingQueue.offer((WebSocketData) o);
}
#Override
public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exception) {
exception.printStackTrace();
}
}
Also make sure your BlockingQueue is using the correct type BlockingQueue<WebSocketData> blockingQueue

Related

How to send message using websocket whenever an API is called using springboot?

I have a simple controller which return name . I have websocket handler which return message to client as: Hey there, presentation recieved from user. whenever http://localhost:8080/sample is called, i need to display the above message to <ws://localhost:8080/presentation>, using https://websocketking.com/ to connect to websocket.
#RestController
public class WebController {
#RequestMapping("/sample")
public SampleResponse Sample(#RequestParam(value = "name",
defaultValue = "Robot") String name) {
SampleResponse response = new SampleResponse();
response.setId(1);
response.setMessage("Your name is "+name);
return response;
}
}
#Component
public class WebSocketHandler extends AbstractWebSocketHandler {
#Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
System.out.println("New Text Message Received from presetation");
String payload = message.getPayload();
System.out.println(payload);
session.sendMessage(new TextMessage("Hey there, presentation recieved from user"));
}
}
public class WebSocketConfiguration implements WebSocketConfigurer {
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new WebSocketHandler(), "/presentation").setAllowedOrigins("*");
}
}

Unit testing Camel/RabbitMQ routes issue

I'm having issue unit testing a camel route which uses rabbitmq for the broker.
I've been researching for weeks but haven't found an effective way to do this.
Firstly, I was having an issue with NOT calling rabbitmq in my test, and to keep this a unit test and not an integration test. This was achieved by using advicewith and switch out the queue for mock queues.
However, with the following code the messages are not reaching the result or end queue (MOBILE_QUEUE).
java.lang.AssertionError: mock://result Received message count. Expected: <1> but was: <0>
Expected :<1>
Actual :<0>
Here is my route, which imports rabbitmq.class
from(TEST_QUEUE).to(MOBILE_QUEUE).routeId("test2phone");
My config rabbitmq.class
#Component
public class RabbitMQ extends Properties {
public final String TEST_QUEUE = CreateRabbitMQQueue("TestQueue", "camel");
public final String MOBILE_QUEUE = CreateRabbitMQQueue("MobileQueue", "camel");
public static String CreateRabbitMQQueue(String QueueName, String RoutingKey)
{
String hostv;
String portv;
String username;
String password;
hostv = "mq-staging";
portv = System.getenv("SERVICE_PORT_AMQP");
username = System.getenv("V_RABBIT_USERNAME");
password = System.getenv("V_RABBIT_PASSWORD");
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromPath("/" )
.scheme("rabbitmq")
.host(hostv)
.port(portv)
.path("/" + QueueName)
.queryParam("username",username)
.queryParam("password", password)
.queryParam("routingKey",RoutingKey)
.queryParam("queue","Q" + QueueName);
return uriBuilder.toUriString();
}
}
And my unit test
#RunWith(CamelSpringRunner.class)
#MockEndpoints
#UseAdviceWith
#SpringBootTest
public class RouteTester extends CamelTestSupport {
String TEST_QUEUE;
String MOBILE_QUEUE;
#Autowired
Routes routes;
#Autowired
CamelContext context;
#Autowired
ProducerTemplate template;
#Before
public void setUp() throws Exception {
TEST_QUEUE = routes.getTEST_QUEUE();
MOBILE_QUEUE = routes.getMOBILE_QUEUE();
context.getRouteDefinition("test2phone").adviceWith(context, new Routes() {
#Override
public void configure() throws Exception {
interceptSendToEndpoint(TEST_QUEUE)
.skipSendToOriginalEndpoint()
.to("mock:testQ");
interceptSendToEndpoint(MOBILE_QUEUE)
.skipSendToOriginalEndpoint()
.to("mock:result");
}
});
context.start();
}
#Test
public void testTest() throws Exception {
String body = "hello123";
MockEndpoint resultEndpoint = context.getEndpoint("mock:result", MockEndpoint.class);
resultEndpoint.expectedMessageCount(1);
resultEndpoint.expectedBodiesReceived(body);
template.sendBody(TEST_QUEUE, body);
resultEndpoint.assertIsSatisfied();
}
#After
public void TearDown() throws Exception {
context.stop();
}
}
interceptSendToEndpoint is useful to intercepting output endpoint. You probably want replace input endpoint and intercept output endpoint. See AdviceWith.
This should work:
context.getRouteDefinition("test2phone").adviceWith(context, new AdviceWithRouteBuilder() {
#Override
public void configure() throws Exception {
replaceFromWith("direct:test");
interceptSendToEndpoint(MOBILE_QUEUE)
.skipSendToOriginalEndpoint()
.to("mock:result");
}
});
And test your route with:
template.sendBody("direct:test", body);

how to use and customize MessageConversion(Spring websocket client)

I wrote a web socket server and a client with spring. The codes is following. The codes sending message to server work, but the sesssion.subscribe method cannot receive message from the server. I search for many documents and check my codes. I don't why it cannot work.
Here is my client codes:
public class Test {
public static void main(String[] args) {
Thread thread = new Thread(new WebsocketThread());
thread.start();
Thread.sleep(5000);
}
}
class MyStompSessionHandler extends StompSessionHandlerAdapter {
#Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
session.send("/app/messages", "{'payload3':2222}".getBytes());
session.subscribe("/user/queue/position-updates", new StompFrameHandler() {
#Override
public Type getPayloadType(StompHeaders headers) {
return String.class;
}
#Override
public void handleFrame(StompHeaders headers, Object payload) {
System.out.println("test:" + payload);
}
});
}
}
class WebsocketThread implements Runnable{
#Override
public void run() {
List<Transport> transports = new ArrayList<>(1);
transports.add(new WebSocketTransport( new StandardWebSocketClient()) );
WebSocketClient webSocketClient = new SockJsClient(transports);
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
String url = "ws://127.0.0.1:8860/orders";
StompSessionHandler sessionHandler = new MyStompSessionHandler();
ListenableFuture<StompSession> future = stompClient.connect(url, sessionHandler);
}
}
Here is my server codes:
#Controller
public class TestController {
#Autowired
private SimpMessagingTemplate simpMessagingTemplate;
#MessageMapping("/messages")
public void sendUserMsg(String messages) throws IOException {
System.out.println("webSocket:" + messages);
simpMessagingTemplate.convertAndSend("/queue/position-updates", "This is return message");
}
}
It is Exception:
org.springframework.messaging.converter.MessageConversionException: No suitable converter, payloadType=class java.lang.String, handlerType=class com.example.hello.MyStompSessionHandler
at org.springframework.messaging.simp.stomp.DefaultStompSession.invokeHandler(DefaultStompSession.java:419)
at org.springframework.messaging.simp.stomp.DefaultStompSession.handleMessage(DefaultStompSession.java:373)
at org.springframework.web.socket.messaging.WebSocketStompClient$WebSocketTcpConnectionHandlerAdapter.handleMessage(WebSocketStompClient.java:342)
at org.springframework.web.socket.sockjs.client.AbstractClientSockJsSession.handleMessageFrame(AbstractClientSockJsSession.java:267)
at org.springframework.web.socket.sockjs.client.AbstractClientSockJsSession.handleFrame(AbstractClientSockJsSession.java:200)
at org.springframework.web.socket.sockjs.client.WebSocketTransport$ClientSockJsWebSocketHandler.handleTextMessage(WebSocketTransport.java:162)
at org.springframework.web.socket.handler.AbstractWebSocketHandler.handleMessage(AbstractWebSocketHandler.java:43)
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.handleTextMessage(StandardWebSocketHandlerAdapter.java:110)
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.access$000(StandardWebSocketHandlerAdapter.java:42)
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:81)
at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:78)
at org.apache.tomcat.websocket.WsFrameBase.sendMessageText(WsFrameBase.java:399)
at org.apache.tomcat.websocket.WsFrameBase.processDataText(WsFrameBase.java:500)
at org.apache.tomcat.websocket.WsFrameBase.processData(WsFrameBase.java:295)
at org.apache.tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.java:131)
at org.apache.tomcat.websocket.WsFrameClient.processSocketRead(WsFrameClient.java:73)
at org.apache.tomcat.websocket.WsFrameClient.access$300(WsFrameClient.java:31)
at org.apache.tomcat.websocket.WsFrameClient$WsFrameClientCompletionHandler.completed(WsFrameClient.java:131)
at org.apache.tomcat.websocket.WsFrameClient$WsFrameClientCompletionHandler.completed(WsFrameClient.java:114)
at sun.nio.ch.Invoker.invokeUnchecked(Invoker.java:126)
at sun.nio.ch.Invoker$2.run(Invoker.java:218)
at sun.nio.ch.AsynchronousChannelGroupImpl$1.run(AsynchronousChannelGroupImpl.java:112)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
add a StringMessageConverter to Client, it works.
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
But how to customize our own MessageConverter? Is there any article?
In my case, the server was sending both json and raw string messages on different channels.
To be able to handle both cases, I went through the MessageConverter implementations and found CompositeMessageConverter, which allows multiple converters to be setup on the client.
Code:
List<MessageConverter> converters = new ArrayList<MessageConverter>();
converters.add(new MappingJackson2MessageConverter()); // used to handle json messages
converters.add(new StringMessageConverter()); // used to handle raw strings
client.setMessageConverter(new CompositeMessageConverter(converters));
The StompFrameHandler will then decide, based on what getPayloadType() returns, which converter to use.
add a SimpleMessageConverter to Client, it works.
stompClient.setMessageConverter(new SimpleMessageConverter());
It seems like you don't have configured any org.springframework.messaging.converter.MessageConverter in web socket configuration.
If you have jackson jar on your class path then it will be automatically picked up for json conversion. For other convertors , you need to configure it in WebSocket Config file .
#Configuration
#EnableWebSocketMessageBroker
#ComponentScan(SpringScanPackageNames)
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
#Override
public boolean configureMessageConverters(List<MessageConverter> arg0) {
StringMessageConverter strConvertor = new StringMessageConverter();
arg0.add(strConvertor);
return true;
}
// Other config
I had a similar problem (though I was sending custom objects) and what worked for me was to simply set the Jackson2Message message converter as
webSocketStompClient.messageConverter = new MappingJackson2MessageConverter()
I found useful info (and examples) about this on: https://github.com/Noozen/spring-boot-websocket-client#the-java-client

Type mismatch: cannot convert from String to ListenableFuture<String>

I'm trying to implementing non-blocking call. in spring 4, But unfortunately it's throwing the below error.
Type mismatch: cannot convert from String to ListenableFuture
and also same error can not able convert from Map to ListenableFuture>.
My Method call stack is as below.
ListenableFuture<Map<String,String>> unusedQuota = doLogin(userIdentity,request,"0");
doLogin login simply return Map
is there any converter required?
what changes would be required ?
Thanks.
public class MyController {
final DeferredResult<Map<String,String>> deferredResult = new DeferredResult<Map<String,String>>(5000l);
private final Logger log = LoggerFactory.getLogger(MyController.class);
#Inject
RestTemplate restTemplate;
#RequestMapping(value = "/loginservice", method = RequestMethod.GET)
#Timed
public DeferredResult<Map<String,String>> loginRequestService(#RequestParam String userIdentity,HttpServletRequest request) throws Exception {
deferredResult.onTimeout(new Runnable() {
#Override
public void run() { // Retry on timeout
deferredResult.setErrorResult(ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT).body("Request timeout occurred."));
}
});
#SuppressWarnings("unchecked")
ListenableFuture<Map<String,String>> unusedQuota = doLogin(userIdentity,request);
unusedQuota.addCallback(new ListenableFutureCallback<Map<String,String>>() {
#SuppressWarnings("unchecked")
#Override
public void onSuccess(Map<String, String> result) {
// TODO Auto-generated method stub
deferredResult.setResult((Map<String, String>) ResponseEntity.ok(result));
}
#Override
public void onFailure(Throwable t) {
// TODO Auto-generated method stub
deferredResult.setErrorResult(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(t));
}
});
return deferredResult;
}
private Map<String,String> doLogin(String userIdentity,HttpServletRequest request) throws Exception{
Map<String,String> unusedQuota=new HashMap<String,String>();
unusedQuota.put("quota", "100");
return unusedQuota;
}
}
}
You are NOT passing the Map object when there is an exception which is causing the issue, so your controller method needs to be changed as shown below, also move deferredResult object inside the Controller method as you should share the same instance of deferredResult for different user request.
public class MyController {
#Autowired
private TaskExecutor asyncTaskExecutor;
#RequestMapping(value = "/loginservice", method = RequestMethod.GET)
#Timed
public DeferredResult<Map<String,String>> loginRequestService(#RequestParam String userIdentity,HttpServletRequest request) throws Exception {
final DeferredResult<Map<String,String>> deferredResult = new DeferredResult<Map<String,String>>(5000l);
deferredResult.onTimeout(new Runnable() {
#Override
public void run() { // Retry on timeout
Map<String, String> map = new HashMap<>();
//Populate map object with error details with Request timeout occurred.
deferredResult.setErrorResult(new ResponseEntity
<Map<String, String>>(map, null,
HttpStatus.REQUEST_TIMEOUT));
}
});
ListenableFuture<String> task = asyncTaskExecutor.submitListenable(new Callable<String>(){
#Override
public Map<String,String> call() throws Exception {
return doLogin(userIdentity,request);
}
});
unusedQuota.addCallback(new ListenableFutureCallback<Map<String,String>>() {
#SuppressWarnings("unchecked")
#Override
public void onSuccess(Map<String, String> result) {
// TODO Auto-generated method stub
deferredResult.setResult((Map<String, String>) ResponseEntity.ok(result));
}
#Override
public void onFailure(Throwable t) {
Map<String, String> map = new HashMap<>();
//Populate map object with error details
deferredResult.setErrorResult(new ResponseEntity<Map<String, String>>(
map, null, HttpStatus.INTERNAL_SERVER_ERROR));
}
});
return deferredResult;
}
}
Also, you need to ensure that you are configuring the ThreadPoolTaskExecutor as explained in the example here.

Accessing HttpSession inside an annotated #WebSocket class on Embedded Jetty 9

How can I access a HttpSession object inside an annotated #WebSocket class in Jetty 9?
I found how to do it using #ServerEndpoint annotation, like here: HttpSession from #ServerEndpoint
Using the #WebSocket annotation, like in the class bellow, how can I do it?
#WebSocket
public class AuctionWebSocket {
// NEED TO ACCESS HttpSession OBJECT INSIDE THESE METHODS:
#OnWebSocketConnect
public void onConnect(Session session) {
System.out.println("onConnect...");
}
#OnWebSocketMessage
public void onMessage(String message) {
System.out.println("Message: " + message);
}
#OnWebSocketClose
public void onClose(int statusCode, String reason) {
System.out.println("onClose...");
}
#OnWebSocketError
public void onError(Throwable t) {
System.out.println("onError...");
}
}
Inside the method onConnect(Session session), I tried to call session.getUpgradeRequest().getSession() which always returns null.
For sake of information, here is how I start embedded Jetty 9:
public class Main {
public static void main(String[] args) throws Exception {
String webPort = System.getenv("PORT");
if (webPort == null || webPort.isEmpty()) {
webPort = "8080";
}
Server server = new Server(Integer.parseInt(webPort));
ClassList classlist = org.eclipse.jetty.webapp.Configuration.ClassList.setServerDefault(server);
classlist.addBefore("org.eclipse.jetty.webapp.JettyWebXmlConfiguration",
"org.eclipse.jetty.annotations.AnnotationConfiguration");
WebAppContext wac = new WebAppContext();
String webappDirLocation = "./src/main/webapp/";
wac.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern", ".*/classes/.*");
wac.setDescriptor(webappDirLocation + "/WEB-INF/web.xml");
wac.setBaseResource(new ResourceCollection(new String[]{webappDirLocation, "./target"}));
wac.setResourceAlias("/WEB-INF/classes/", "/classes/");
wac.setContextPath("/");
wac.setParentLoaderPriority(true);
/*
* WebSocket handler.
*/
WebSocketHandler wsh = new WebSocketHandler() {
#Override
public void configure(WebSocketServletFactory wssf) {
wssf.register(AuctionWebSocket.class);
}
};
ContextHandler wsc = new ContextHandler();
wsc.setContextPath("/auction-notifications");
wsc.setHandler(wsh);
ContextHandlerCollection chc = new ContextHandlerCollection();
chc.setHandlers(new Handler[]{wac, wsc});
server.setHandler(chc);
server.start();
server.join();
}
}
Let me know if you need more information.
Any help will be appreciated.
Thanks in advance.
You'll want to use the WebSocketCreator concepts.
First you set the WebSocketCreator of your choice in the WebSocketServletFactory that you configure in your WebSocketServlet
public class MySessionSocketServlet extends WebSocketServlet
{
#Override
public void configure(WebSocketServletFactory factory)
{
factory.getPolicy().setIdleTimeout(30000);
factory.setCreator(new MySessionSocketCreator());
}
}
Next, you'll want to grab the HttpSession during the upgrade and pass it into the WebSocket object that you are creating.
public class MySessionSocketCreator implements WebSocketCreator
{
#Override
public Object createWebSocket(ServletUpgradeRequest req, ServletUpgradeResponse resp)
{
HttpSession httpSession = req.getSession();
return new MySessionSocket(httpSession);
}
}
Finally, just keep track of that HttpSession in your own WebSocket.
#WebSocket
public class MySessionSocket
{
private HttpSession httpSession;
private Session wsSession;
public MySessionSocket(HttpSession httpSession)
{
this.httpSession = httpSession;
}
#OnWebSocketConnect
public void onOpen(Session wsSession)
{
this.wsSession = wsSession;
}
}
Of note: the HttpSession can expire and be scavenged and cleaned up while a WebSocket is active. Also, the HttpSession contents at this point are not guaranteed to be kept in sync with changes from other web actions (this mostly depends on what Session storage / caching technology you use on the server side)
And one more note: resist the urge to store / track the ServletUpgradeRequest object in your Socket instance, as this object is recycled and cleaned up aggressively by Jetty proper.

Resources