org.appverse.web.framework.backend.frontfacade.websocket.IntegrationWebsocketTest.java Source code

Java tutorial

Introduction

Here is the source code for org.appverse.web.framework.backend.frontfacade.websocket.IntegrationWebsocketTest.java

Source

package org.appverse.web.framework.backend.frontfacade.websocket;/*
                                                                 Copyright (c) 2012 GFT Appverse, S.L., Sociedad Unipersonal.
                                                                     
                                                                 This Source Code Form is subject to the terms of the Appverse Public License 
                                                                 Version 2.0 (APL v2.0?). If a copy of the APL was not distributed with this 
                                                                 file, You can obtain one at http://www.appverse.mobi/licenses/apl_v2.0.pdf. [^]
                                                                     
                                                                 Redistribution and use in source and binary forms, with or without modification, 
                                                                 are permitted provided that the conditions of the AppVerse Public License v2.0 
                                                                 are met.
                                                                     
                                                                 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
                                                                 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
                                                                 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
                                                                 DISCLAIMED. EXCEPT IN CASE OF WILLFUL MISCONDUCT OR GROSS NEGLIGENCE, IN NO EVENT
                                                                 SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
                                                                 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
                                                                 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
                                                                 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
                                                                 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT(INCLUDING NEGLIGENCE OR OTHERWISE) 
                                                                 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
                                                                 POSSIBILITY OF SUCH DAMAGE.
                                                                 */

import static org.junit.Assert.*;

import java.io.IOException;
import java.lang.reflect.Type;
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 org.appverse.web.framework.backend.frontfacade.websocket.config.DispatcherServletInitializer;
import org.appverse.web.framework.backend.frontfacade.websocket.model.PortfolioPosition;
import org.appverse.web.framework.backend.frontfacade.websocket.model.Trade;
import org.appverse.web.framework.backend.frontfacade.websocket.support.TomcatWebSocketTestServer;
import org.appverse.web.framework.backend.frontfacade.websocket.support.WebSecurityInitializer;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompFrameHandler;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompSessionHandler;
import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
import org.springframework.scheduling.annotation.EnableScheduling;
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.RequestCallback;
import org.springframework.web.client.ResponseExtractor;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration;
import org.springframework.web.socket.WebSocketHttpHeaders;
import org.springframework.web.socket.client.WebSocketClient;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.messaging.WebSocketStompClient;
import org.springframework.web.socket.server.standard.TomcatRequestUpgradeStrategy;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
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;

/**
 * End-to-end integration tests that run an embedded Tomcat server and establish
 * an actual WebSocket session using
 * {@link org.springframework.web.socket.client.standard.StandardWebSocketClient}.
 * as well as a simple STOMP/WebSocket client created to support these tests.
 *
 * The test strategy here is to test from the perspective of a client connecting
 * to a server and therefore it is a much more complete test. However, writing
 * and maintaining these tests is a bit more involved.
 *
 * An all-encapsulating strategy might be to write the majority of tests using
 * server-side testing (either standalone or with Spring configuration) with
 * end-to-end integration tests serving as a higher-level verification but
 * overall fewer in number.
 *
 * @author Rossen Stoyanchev
 */
@Ignore
public class IntegrationWebsocketTest {

    private final static WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
    private static Logger logger = LoggerFactory.getLogger(IntegrationWebsocketTest.class);
    private static int port;
    private static TomcatWebSocketTestServer server;
    private static SockJsClient sockJsClient;
    private static WebSocketClient standardWebSocketClient;

    @BeforeClass
    public static void setup() throws Exception {

        // Since test classpath includes both embedded Tomcat and Jetty we need to
        // set a Spring profile explicitly to bypass WebSocket engine detection.
        // See {@link org.springframework.samples.portfolio.config.WebSocketConfig}

        // This test is not supported with Jetty because it doesn't seem to support
        // deployment withspecific ServletContainerInitializer's at for testing

        System.setProperty("spring.profiles.active", "test.tomcat");

        port = SocketUtils.findAvailableTcpPort();

        server = new TomcatWebSocketTestServer(port);
        server.deployConfig(TestDispatcherServletInitializer.class, WebSecurityInitializer.class);
        server.start();

        loginAndSaveJsessionIdCookie("fabrice", "fab123", 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);
    }

    private static void loginAndSaveJsessionIdCookie(final String user, final String password,
            final HttpHeaders headersToUpdate) {

        String url = "http://localhost:" + port + "/";

        new RestTemplate().execute(url, HttpMethod.POST,

                new RequestCallback() {
                    @Override
                    public void doWithRequest(ClientHttpRequest request) throws IOException {
                        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
                        map.add("username", user);
                        map.add("password", password);
                        new FormHttpMessageConverter().write(map, MediaType.APPLICATION_FORM_URLENCODED, request);
                    }
                },

                new ResponseExtractor<Object>() {
                    @Override
                    public Object extractData(ClientHttpResponse response) throws IOException {
                        headersToUpdate.add("Cookie", response.getHeaders().getFirst("Set-Cookie"));
                        return null;
                    }
                });
    }

    @AfterClass
    public static void teardown() throws Exception {
        if (server != null) {
            try {
                server.undeployConfig();
            } catch (Throwable t) {
                logger.error("Failed to undeploy application", t);
            }

            try {
                server.stop();
            } catch (Throwable t) {
                logger.error("Failed to stop server", t);
            }
        }
    }

    @Test
    public void getPositions() throws Exception {

        final CountDownLatch latch = new CountDownLatch(1);
        final AtomicReference<Throwable> failure = new AtomicReference<>();

        StompSessionHandler handler = new AbstractTestSessionHandler(failure) {

            @Override
            public void afterConnected(final StompSession session, StompHeaders connectedHeaders) {
                session.subscribe("/app/positions", new StompFrameHandler() {
                    @Override
                    public Type getPayloadType(StompHeaders headers) {
                        return byte[].class;
                    }

                    @Override
                    public void handleFrame(StompHeaders headers, Object payload) {
                        String json = new String((byte[]) payload);
                        logger.debug("Got " + json);
                        try {
                            new JsonPathExpectationsHelper("$[0].company").assertValue(json,
                                    "Citrix Systems, Inc.");
                            new JsonPathExpectationsHelper("$[1].company").assertValue(json, "Dell Inc.");
                            new JsonPathExpectationsHelper("$[2].company").assertValue(json, "Microsoft");
                            new JsonPathExpectationsHelper("$[3].company").assertValue(json, "Oracle");
                        } catch (Throwable t) {
                            failure.set(t);
                        } finally {
                            session.disconnect();
                            latch.countDown();
                        }
                    }
                });
            }
        };

        WebSocketStompClient stompClient = new WebSocketStompClient(sockJsClient);
        stompClient.connect("http://localhost:{port}/services/websocket", this.headers, handler, port);

        if (failure.get() != null) {
            throw new AssertionError("", failure.get());
        }

        if (!latch.await(5, TimeUnit.SECONDS)) {
            fail("Portfolio positions not received");
        }
    }

    @Test
    public void executeTrade() throws Exception {

        final CountDownLatch latch = new CountDownLatch(1);
        final AtomicReference<Throwable> failure = new AtomicReference<Throwable>();

        StompSessionHandler handler = new AbstractTestSessionHandler(failure) {

            @Override
            public void afterConnected(final StompSession session, StompHeaders connectedHeaders) {
                session.subscribe("/user/queue/position-updates", new StompFrameHandler() {
                    @Override
                    public Type getPayloadType(StompHeaders headers) {
                        return PortfolioPosition.class;
                    }

                    @Override
                    public void handleFrame(StompHeaders headers, Object payload) {
                        PortfolioPosition position = (PortfolioPosition) payload;
                        logger.debug("Got " + position);
                        try {
                            assertEquals(75, position.getShares());
                            assertEquals("Dell Inc.", position.getCompany());
                        } catch (Throwable t) {
                            failure.set(t);
                        } finally {
                            session.disconnect();
                            latch.countDown();
                        }
                    }
                });

                try {
                    Trade trade = new Trade();
                    trade.setAction(Trade.TradeAction.Buy);
                    trade.setTicker("DELL");
                    trade.setShares(25);
                    session.send("/app/trade", trade);
                } catch (Throwable t) {
                    failure.set(t);
                    latch.countDown();
                }
            }
        };
        //test websocket
        WebSocketStompClient stompClient = new WebSocketStompClient(sockJsClient);
        stompClient.setMessageConverter(new MappingJackson2MessageConverter());
        stompClient.connect("ws://localhost:{port}/services/websocket/standard", headers, handler, port);

        if (!latch.await(10, TimeUnit.SECONDS)) {
            fail("Trade confirmation not received");
        } else if (failure.get() != null) {
            throw new AssertionError("", failure.get());
        }
        //test sockJs
        stompClient = new WebSocketStompClient(sockJsClient);
        stompClient.setMessageConverter(new MappingJackson2MessageConverter());
        stompClient.connect("http://localhost:{port}/services/websocket/sockJs", headers, handler, port);

        if (!latch.await(10, TimeUnit.SECONDS)) {
            fail("Trade confirmation not received");
        } else if (failure.get() != null) {
            throw new AssertionError("", failure.get());
        }

    }

    public static class TestDispatcherServletInitializer extends DispatcherServletInitializer {

        @Override
        protected Class<?>[] getServletConfigClasses() {
            return new Class[] { DelegatingWebMvcConfiguration.class, TestWebSocketConfig.class };
        }
    }

    @Configuration
    @EnableScheduling
    @ComponentScan(basePackages = "org.springframework.samples", excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = Configuration.class))
    @EnableWebSocketMessageBroker
    static class TestWebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

        @Autowired
        Environment env;

        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            // The test classpath includes both Tomcat and Jetty, so let's be explicit
            DefaultHandshakeHandler handler = new DefaultHandshakeHandler(new TomcatRequestUpgradeStrategy());
            registry.addEndpoint("/services/websocket").setHandshakeHandler(handler).withSockJS();
        }

        @Override
        public void configureMessageBroker(MessageBrokerRegistry registry) {
            //         registry.enableSimpleBroker("/queue/", "/topic/");
            registry.enableStompBrokerRelay("/queue/", "/topic/");
            registry.setApplicationDestinationPrefixes("/app");
        }
    }

    private static abstract class AbstractTestSessionHandler extends StompSessionHandlerAdapter {

        private final AtomicReference<Throwable> failure;

        public AbstractTestSessionHandler(AtomicReference<Throwable> failure) {
            this.failure = failure;
        }

        @Override
        public void handleFrame(StompHeaders headers, Object payload) {
            logger.error("STOMP ERROR frame: " + headers.toString());
            this.failure.set(new Exception(headers.toString()));
        }

        @Override
        public void handleException(StompSession s, StompCommand c, StompHeaders h, byte[] p, Throwable ex) {
            logger.error("Handler exception", ex);
            this.failure.set(ex);
        }

        @Override
        public void handleTransportError(StompSession session, Throwable ex) {
            logger.error("Transport failure", ex);
            this.failure.set(ex);
        }
    }

}