/* * Copyright 2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.bearchoke.platform.tests.web.websocket; import com.bearchoke.platform.server.common.ServerConstants; import com.bearchoke.platform.server.frontend.FrontendWebApplicationInitializer; import com.bearchoke.platform.tests.web.websocket.support.client.StompMessageHandler; import com.bearchoke.platform.tests.web.websocket.support.client.StompSession; import com.bearchoke.platform.tests.web.websocket.support.client.WebSocketStompClient; import com.bearchoke.platform.tests.web.websocket.support.server.TomcatWebSocketTestServer; import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.RandomStringUtils; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.messaging.Message; import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.test.util.JsonPathExpectationsHelper; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.SocketUtils; import org.springframework.web.client.RestTemplate; import org.springframework.web.socket.WebSocketHttpHeaders; import org.springframework.web.socket.client.standard.StandardWebSocketClient; import org.springframework.web.socket.sockjs.client.RestTemplateXhrTransport; import org.springframework.web.socket.sockjs.client.SockJsClient; import org.springframework.web.socket.sockjs.client.Transport; import org.springframework.web.socket.sockjs.client.WebSocketTransport; import java.net.URI; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.fail; @Log4j2 public class QuotesWebSocketIntegrationTest { private static int port; private static TomcatWebSocketTestServer server; private static SockJsClient sockJsClient; private final static WebSocketHttpHeaders headers = new WebSocketHttpHeaders(); @BeforeClass public static void setup() throws Exception { log.info("Setting up Quotes Web Socket Integration test...."); port = SocketUtils.findAvailableTcpPort(); server = new TomcatWebSocketTestServer(port); server.deployConfig(FrontendWebApplicationInitializer.class); server.start(); loginAndSaveXAuthToken("harrymitchell", "HarryMitchell5!", headers); List<Transport> transports = new ArrayList<>(); transports.add(new WebSocketTransport(new StandardWebSocketClient())); RestTemplateXhrTransport xhrTransport = new RestTemplateXhrTransport(new RestTemplate()); xhrTransport.setRequestHeaders(headers); transports.add(xhrTransport); sockJsClient = new SockJsClient(transports); sockJsClient.setHttpHeaderNames("X-Auth-Token"); log.info("Setup complete!"); } private static void loginAndSaveXAuthToken(final String user, final String password, final HttpHeaders headersToUpdate) { log.info("Authenticating user before subscribing to web socket"); String url = "http://dev.bearchoke.com:" + port + "/api/authenticate"; try { new RestTemplate().execute(url, HttpMethod.POST, request -> { MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); map.add("username", user); map.add("password", password); new FormHttpMessageConverter().write(map, MediaType.APPLICATION_FORM_URLENCODED, request); }, response -> { String xAuthToken = response.getHeaders().getFirst(ServerConstants.X_AUTH_TOKEN); log.info("Retrieved x-auth-token: " + xAuthToken); headersToUpdate.add(ServerConstants.X_AUTH_TOKEN, xAuthToken); return null; }); } catch (Exception ex) { log.error(ex.getMessage(), ex); } } @AfterClass public static void teardown() throws Exception { log.info("Tearing down server after integration test complete"); if (server != null) { try { server.undeployConfig(); } catch (Throwable t) { log.error("Failed to undeploy application", t); } try { server.stop(); } catch (Throwable t) { log.error("Failed to stop server", t); } } } @Test public void getQuotes() throws Exception { log.info("Testing getting stock quotes from QuoteService..."); final CountDownLatch latch = new CountDownLatch(1); final AtomicReference<Throwable> failure = new AtomicReference<Throwable>(); URI uri = new URI("ws://dev.bearchoke.com:" + port + "/ws"); WebSocketStompClient stompClient = new WebSocketStompClient(uri, this.headers, sockJsClient); stompClient.setMessageConverter(new MappingJackson2MessageConverter()); stompClient.connect(new StompMessageHandler() { private StompSession stompSession; @Override public void afterConnected(StompSession stompSession, StompHeaderAccessor headers) { String channel = "/topic/price.stock.*"; QuotesWebSocketIntegrationTest.log.info("Subscribing to channel: " + channel); stompSession.subscribe(channel, RandomStringUtils.randomAlphabetic(10)); this.stompSession = stompSession; } @Override public void handleMessage(Message<byte[]> message) { StompHeaderAccessor headers = StompHeaderAccessor.wrap(message); if (!headers.getDestination().startsWith("/topic/price.stock.")) { failure.set(new IllegalStateException("Unexpected message: " + message)); } QuotesWebSocketIntegrationTest.log.debug("Got \n" + new String(message.getPayload())); try { String json = new String(message.getPayload(), Charset.forName("UTF-8")); new JsonPathExpectationsHelper("$.company").assertValue(json, "Citrix Systems, Inc."); new JsonPathExpectationsHelper("$.ticker").assertValue(json, "CTXS"); } catch (Throwable t) { failure.set(t); } finally { this.stompSession.disconnect(); latch.countDown(); } } @Override public void handleError(Message<byte[]> message) { failure.set(new Exception(new String(message.getPayload(), Charset.forName("UTF-8")))); } @Override public void handleReceipt(String receiptId) {} @Override public void afterDisconnected() { QuotesWebSocketIntegrationTest.log.info("Successfully disconnected from Web Socket channel"); } }); if (failure.get() != null) { throw new AssertionError("", failure.get()); } if (!latch.await(5, TimeUnit.SECONDS)) { Assert.fail("Quotes not received"); } } }