From 05084d504bca9351f39c949de45378ede28ff272 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 14 May 2013 12:00:51 -0400 Subject: [PATCH] Add spring-websocket module tests --- .../http/server/ServletServerHttpRequest.java | 1 + .../client/ConnectionManagerSupport.java | 32 +-- .../client/WebSocketConnectionManager.java | 2 +- .../endpoint/StandardWebSocketClient.java | 76 +++-- ...orter.java => ServerEndpointExporter.java} | 8 +- ...n.java => ServerEndpointRegistration.java} | 93 +++--- .../ServletServerContainerFactoryBean.java | 7 +- .../server/endpoint/SpringConfigurator.java | 6 +- .../web/socket/server/endpoint/Test.java | 6 + .../socket/server/endpoint/package-info.java | 4 +- .../GlassFishRequestUpgradeStrategy.java | 4 +- .../support/TomcatRequestUpgradeStrategy.java | 4 +- .../sockjs/AbstractServerSockJsSession.java | 162 ----------- .../socket/sockjs/AbstractSockJsService.java | 2 +- .../socket/sockjs/AbstractSockJsSession.java | 161 +++++++++-- .../web/socket/sockjs/SockJsFrame.java | 17 ++ .../web/socket/sockjs/TransportType.java | 2 +- .../sockjs/support/DefaultSockJsService.java | 19 +- ...AbstractHttpReceivingTransportHandler.java | 20 +- .../AbstractHttpSendingTransportHandler.java | 9 +- ...on.java => AbstractHttpSockJsSession.java} | 8 +- .../EventSourceTransportHandler.java | 4 +- .../transport/HtmlFileTransportHandler.java | 6 +- .../JsonpPollingTransportHandler.java | 6 +- .../transport/JsonpTransportHandler.java | 18 +- ...Session.java => PollingSockJsSession.java} | 4 +- ...ssion.java => StreamingSockJsSession.java} | 4 +- .../WebSocketServerSockJsSession.java | 9 +- .../transport/XhrPollingTransportHandler.java | 4 +- .../XhrStreamingTransportHandler.java | 4 +- .../support/WebSocketHandlerDecorator.java | 2 +- .../web/socket/AbstractHttpRequestTests.java | 13 +- .../JettyWebSocketListenerAdapterTests.java | 71 +++++ .../adapter/StandardEndpointAdapterTests.java | 82 ++++++ .../WebSocketConnectionManagerTests.java | 159 +++++++++++ .../StandardWebSocketClientTests.java | 89 ++++++ .../endpoint/ServerEndpointExporterTests.java | 110 +++++++ .../ServerEndpointRegistrationTests.java | 93 ++++++ .../endpoint/SpringConfiguratorTests.java | 127 +++++++++ .../sockjs/AbstractSockJsServiceTests.java | 151 ++++++++-- .../sockjs/AbstractSockJsSessionTests.java | 269 ++++++++++++++++++ .../BaseAbstractSockJsSessionTests.java | 74 +++++ .../web/socket/sockjs/TestSockJsSession.java | 107 +++++++ .../support/DefaultSockJsServiceTests.java | 146 +++++++++- .../AbstractHttpSockJsSessionTests.java | 176 ++++++++++++ .../HttpReceivingTransportHandlerTests.java | 139 +++++++++ .../HttpSendingTransportHandlerTests.java | 186 ++++++++++++ .../WebSocketServerSockJsSessionTests.java | 153 ++++++++++ .../BeanCreatingHandlerProviderTests.java | 96 +++++++ ...ceptionWebSocketHandlerDecoratorTests.java | 102 +++++++ .../PerConnectionWebSocketHandlerTests.java | 82 ++++++ .../socket/support/TestWebSocketSession.java | 185 ++++++++++++ 52 files changed, 2927 insertions(+), 387 deletions(-) rename spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/{EndpointExporter.java => ServerEndpointExporter.java} (94%) rename spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/{EndpointRegistration.java => ServerEndpointRegistration.java} (66%) create mode 100644 spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/Test.java delete mode 100644 spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractServerSockJsSession.java rename spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/{AbstractHttpServerSockJsSession.java => AbstractHttpSockJsSession.java} (95%) rename spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/{PollingServerSockJsSession.java => PollingSockJsSession.java} (87%) rename spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/{StreamingServerSockJsSession.java => StreamingSockJsSession.java} (93%) create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/adapter/JettyWebSocketListenerAdapterTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/adapter/StandardEndpointAdapterTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/client/WebSocketConnectionManagerTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/client/endpoint/StandardWebSocketClientTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/server/endpoint/ServerEndpointExporterTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/server/endpoint/ServerEndpointRegistrationTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/server/endpoint/SpringConfiguratorTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsSessionTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/sockjs/BaseAbstractSockJsSessionTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/sockjs/TestSockJsSession.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSockJsSessionTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/HttpReceivingTransportHandlerTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/HttpSendingTransportHandlerTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSessionTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/support/BeanCreatingHandlerProviderTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/support/ExceptionWebSocketHandlerDecoratorTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/support/PerConnectionWebSocketHandlerTests.java create mode 100644 spring-websocket/src/test/java/org/springframework/web/socket/support/TestWebSocketSession.java diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java index cd10f179e0..eb97233b73 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java @@ -166,6 +166,7 @@ public class ServletServerHttpRequest implements ServerHttpRequest { @Override public MultiValueMap getQueryParams() { if (this.queryParams == null) { + // TODO: extract from query string this.queryParams = new LinkedMultiValueMap(this.servletRequest.getParameterMap().size()); for (String name : this.servletRequest.getParameterMap().keySet()) { for (String value : this.servletRequest.getParameterValues(name)) { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/ConnectionManagerSupport.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/ConnectionManagerSupport.java index f01c04ce14..665592f992 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/client/ConnectionManagerSupport.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/ConnectionManagerSupport.java @@ -26,7 +26,7 @@ import org.springframework.core.task.TaskExecutor; import org.springframework.web.util.UriComponentsBuilder; /** - * Abstract base class for WebSocketConnection managers. + * Abstract base class for WebSocket connection managers. * * @author Rossen Stoyanchev * @since 4.0 @@ -147,25 +147,25 @@ public abstract class ConnectionManagerSupport implements SmartLifecycle { public final void stop() { synchronized (this.lifecycleMonitor) { if (isRunning()) { - stopInternal(); + if (logger.isDebugEnabled()) { + logger.debug("Stopping " + this.getClass().getSimpleName()); + } + try { + stopInternal(); + } + catch (Throwable e) { + logger.error("Failed to stop WebSocket connection", e); + } + finally { + this.isRunning = false; + } } } } - protected void stopInternal() { - if (logger.isDebugEnabled()) { - logger.debug("Stopping " + this.getClass().getSimpleName()); - } - try { - if (isConnected()) { - closeConnection(); - } - } - catch (Throwable e) { - logger.error("Failed to stop WebSocket connection", e); - } - finally { - this.isRunning = false; + protected void stopInternal() throws Exception { + if (isConnected()) { + closeConnection(); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketConnectionManager.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketConnectionManager.java index 1ee44acf41..429833c1d1 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketConnectionManager.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/WebSocketConnectionManager.java @@ -84,7 +84,7 @@ public class WebSocketConnectionManager extends ConnectionManagerSupport { } @Override - public void stopInternal() { + public void stopInternal() throws Exception { if (this.syncClientLifecycle) { ((SmartLifecycle) client).stop(); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/endpoint/StandardWebSocketClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/endpoint/StandardWebSocketClient.java index 21678e12ef..5fc60b4520 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/client/endpoint/StandardWebSocketClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/endpoint/StandardWebSocketClient.java @@ -52,12 +52,15 @@ public class StandardWebSocketClient implements WebSocketClient { private static final Log logger = LogFactory.getLog(StandardWebSocketClient.class); - private static final Set EXCLUDED_HEADERS = new HashSet( - Arrays.asList("Sec-WebSocket-Accept", "Sec-WebSocket-Extensions", "Sec-WebSocket-Key", - "Sec-WebSocket-Protocol", "Sec-WebSocket-Version")); + private WebSocketContainer webSocketContainer; - private WebSocketContainer webSocketContainer = ContainerProvider.getWebSocketContainer(); + public WebSocketContainer getWebSocketContainer() { + if (this.webSocketContainer == null) { + this.webSocketContainer = ContainerProvider.getWebSocketContainer(); + } + return this.webSocketContainer; + } public void setWebSocketContainer(WebSocketContainer container) { this.webSocketContainer = container; @@ -72,8 +75,8 @@ public class StandardWebSocketClient implements WebSocketClient { } @Override - public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, - final HttpHeaders httpHeaders, URI uri) throws WebSocketConnectFailureException { + public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, HttpHeaders httpHeaders, URI uri) + throws WebSocketConnectFailureException { StandardWebSocketSessionAdapter session = new StandardWebSocketSessionAdapter(); session.setUri(uri); @@ -86,29 +89,7 @@ public class StandardWebSocketClient implements WebSocketClient { if (!protocols.isEmpty()) { configBuidler.preferredSubprotocols(protocols); } - configBuidler.configurator(new Configurator() { - @Override - public void beforeRequest(Map> headers) { - for (String headerName : httpHeaders.keySet()) { - if (!EXCLUDED_HEADERS.contains(headerName)) { - List value = httpHeaders.get(headerName); - if (logger.isTraceEnabled()) { - logger.trace("Adding header [" + headerName + "=" + value + "]"); - } - headers.put(headerName, value); - } - } - if (logger.isTraceEnabled()) { - logger.trace("Handshake request headers: " + headers); - } - } - @Override - public void afterResponse(HandshakeResponse handshakeResponse) { - if (logger.isTraceEnabled()) { - logger.trace("Handshake response headers: " + handshakeResponse.getHeaders()); - } - } - }); + configBuidler.configurator(new StandardWebSocketClientConfigurator(httpHeaders)); } try { @@ -121,4 +102,41 @@ public class StandardWebSocketClient implements WebSocketClient { } } + + private static class StandardWebSocketClientConfigurator extends Configurator { + + private static final Set EXCLUDED_HEADERS = new HashSet( + Arrays.asList("Sec-WebSocket-Accept", "Sec-WebSocket-Extensions", "Sec-WebSocket-Key", + "Sec-WebSocket-Protocol", "Sec-WebSocket-Version")); + + private final HttpHeaders httpHeaders; + + + public StandardWebSocketClientConfigurator(HttpHeaders httpHeaders) { + this.httpHeaders = httpHeaders; + } + + @Override + public void beforeRequest(Map> headers) { + for (String headerName : this.httpHeaders.keySet()) { + if (!EXCLUDED_HEADERS.contains(headerName)) { + List value = this.httpHeaders.get(headerName); + if (logger.isTraceEnabled()) { + logger.trace("Adding header [" + headerName + "=" + value + "]"); + } + headers.put(headerName, value); + } + } + if (logger.isTraceEnabled()) { + logger.trace("Handshake request headers: " + headers); + } + } + @Override + public void afterResponse(HandshakeResponse handshakeResponse) { + if (logger.isTraceEnabled()) { + logger.trace("Handshake response headers: " + handshakeResponse.getHeaders()); + } + } + } + } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/EndpointExporter.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/ServerEndpointExporter.java similarity index 94% rename from spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/EndpointExporter.java rename to spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/ServerEndpointExporter.java index 0e1ce1029e..eb1201f56c 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/EndpointExporter.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/ServerEndpointExporter.java @@ -48,12 +48,13 @@ import org.springframework.util.ReflectionUtils; * @author Rossen Stoyanchev * @since 4.0 */ -public class EndpointExporter implements InitializingBean, BeanPostProcessor, ApplicationContextAware { +public class ServerEndpointExporter implements InitializingBean, BeanPostProcessor, ApplicationContextAware { private static final boolean isServletApiPresent = - ClassUtils.isPresent("javax.servlet.ServletContext", EndpointExporter.class.getClassLoader()); + ClassUtils.isPresent("javax.servlet.ServletContext", ServerEndpointExporter.class.getClassLoader()); + + private static Log logger = LogFactory.getLog(ServerEndpointExporter.class); - private static Log logger = LogFactory.getLog(EndpointExporter.class); private final List> annotatedEndpointClasses = new ArrayList>(); @@ -63,6 +64,7 @@ public class EndpointExporter implements InitializingBean, BeanPostProcessor, Ap private ServerContainer serverContainer; + /** * TODO * @param annotatedEndpointClasses diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/EndpointRegistration.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/ServerEndpointRegistration.java similarity index 66% rename from spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/EndpointRegistration.java rename to spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/ServerEndpointRegistration.java index 40ec854e65..0bcdbd79ca 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/EndpointRegistration.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/ServerEndpointRegistration.java @@ -38,16 +38,14 @@ import org.springframework.web.socket.support.BeanCreatingHandlerProvider; /** * An implementation of {@link javax.websocket.server.ServerEndpointConfig} that also - * holds the target {@link javax.websocket.Endpoint} as a reference or a bean name. - * - *

- * Beans of this type are detected by {@link EndpointExporter} and - * registered with a Java WebSocket runtime at startup. + * holds the target {@link javax.websocket.Endpoint} provided as a reference or as a bean + * name. Beans of this type are detected by {@link ServerEndpointExporter} and registered + * with a Java WebSocket runtime at startup. * * @author Rossen Stoyanchev * @since 4.0 */ -public class EndpointRegistration implements ServerEndpointConfig, BeanFactoryAware { +public class ServerEndpointRegistration implements ServerEndpointConfig, BeanFactoryAware { private final String path; @@ -65,7 +63,7 @@ public class EndpointRegistration implements ServerEndpointConfig, BeanFactoryAw private final Map userProperties = new HashMap(); - private Configurator configurator = new Configurator() {}; + private Configurator configurator = new EndpointRegistrationConfigurator(); /** @@ -74,7 +72,7 @@ public class EndpointRegistration implements ServerEndpointConfig, BeanFactoryAw * @param path * @param endpointClass */ - public EndpointRegistration(String path, Class endpointClass) { + public ServerEndpointRegistration(String path, Class endpointClass) { Assert.hasText(path, "path must not be empty"); Assert.notNull(endpointClass, "endpointClass is required"); this.path = path; @@ -82,7 +80,7 @@ public class EndpointRegistration implements ServerEndpointConfig, BeanFactoryAw this.endpoint = null; } - public EndpointRegistration(String path, Endpoint endpoint) { + public ServerEndpointRegistration(String path, Endpoint endpoint) { Assert.hasText(path, "path must not be empty"); Assert.notNull(endpoint, "endpoint is required"); this.path = path; @@ -152,38 +150,9 @@ public class EndpointRegistration implements ServerEndpointConfig, BeanFactoryAw return this.decoders; } - /** - * The {@link Configurator#getEndpointInstance(Class)} method is always ignored. - */ - public void setConfigurator(Configurator configurator) { - this.configurator = configurator; - } - @Override public Configurator getConfigurator() { - return new Configurator() { - @SuppressWarnings("unchecked") - @Override - public T getEndpointInstance(Class clazz) throws InstantiationException { - return (T) EndpointRegistration.this.getEndpoint(); - } - @Override - public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { - EndpointRegistration.this.configurator.modifyHandshake(sec, request, response); - } - @Override - public boolean checkOrigin(String originHeaderValue) { - return EndpointRegistration.this.configurator.checkOrigin(originHeaderValue); - } - @Override - public String getNegotiatedSubprotocol(List supported, List requested) { - return EndpointRegistration.this.configurator.getNegotiatedSubprotocol(supported, requested); - } - @Override - public List getNegotiatedExtensions(List installed, List requested) { - return EndpointRegistration.this.configurator.getNegotiatedExtensions(installed, requested); - } - }; + return this.configurator; } @Override @@ -193,4 +162,50 @@ public class EndpointRegistration implements ServerEndpointConfig, BeanFactoryAw } } + protected void modifyHandshake(HandshakeRequest request, HandshakeResponse response) { + this.configurator.modifyHandshake(this, request, response); + } + + protected boolean checkOrigin(String originHeaderValue) { + return this.configurator.checkOrigin(originHeaderValue); + } + + protected String getNegotiatedSubprotocol(List supported, List requested) { + return this.configurator.getNegotiatedSubprotocol(supported, requested); + } + + protected List getNegotiatedExtensions(List installed, List requested) { + return this.configurator.getNegotiatedExtensions(installed, requested); + } + + + private class EndpointRegistrationConfigurator extends Configurator { + + @SuppressWarnings("unchecked") + @Override + public T getEndpointInstance(Class clazz) throws InstantiationException { + return (T) ServerEndpointRegistration.this.getEndpoint(); + } + + @Override + public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { + super.modifyHandshake(sec, request, response); + } + + @Override + public boolean checkOrigin(String originHeaderValue) { + return super.checkOrigin(originHeaderValue); + } + + @Override + public String getNegotiatedSubprotocol(List supported, List requested) { + return super.getNegotiatedSubprotocol(supported, requested); + } + + @Override + public List getNegotiatedExtensions(List installed, List requested) { + return super.getNegotiatedExtensions(installed, requested); + } + } + } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/ServletServerContainerFactoryBean.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/ServletServerContainerFactoryBean.java index 7ea6cefe7f..6f802ebcb6 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/ServletServerContainerFactoryBean.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/ServletServerContainerFactoryBean.java @@ -34,7 +34,7 @@ import org.springframework.web.socket.sockjs.SockJsService; * using its setters allows configuring the {@code ServerContainer} through Spring * configuration. This is useful even if the ServerContainer is not injected into any * other bean. For example, an application can configure a {@link DefaultHandshakeHandler} - * , a {@link SockJsService}, or {@link EndpointExporter}, and separately declare this + * , a {@link SockJsService}, or {@link ServerEndpointExporter}, and separately declare this * FactoryBean in order to customize the properties of the (one and only) * {@code ServerContainer} instance. * @@ -44,9 +44,6 @@ import org.springframework.web.socket.sockjs.SockJsService; public class ServletServerContainerFactoryBean implements FactoryBean, InitializingBean, ServletContextAware { - private static final String SERVER_CONTAINER_ATTR_NAME = "javax.websocket.server.ServerContainer"; - - private Long asyncSendTimeout; private Long maxSessionIdleTimeout; @@ -92,7 +89,7 @@ public class ServletServerContainerFactoryBean @Override public void setServletContext(ServletContext servletContext) { - this.serverContainer = (ServerContainer) servletContext.getAttribute(SERVER_CONTAINER_ATTR_NAME); + this.serverContainer = (ServerContainer) servletContext.getAttribute("javax.websocket.server.ServerContainer"); } @Override diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/SpringConfigurator.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/SpringConfigurator.java index dd6efb56ec..b9c2a829be 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/SpringConfigurator.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/SpringConfigurator.java @@ -27,9 +27,9 @@ import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; /** - * This should be used in conjuction with {@link ServerEndpoint @ServerEndpoint} classes. + * This should be used in conjunction with {@link ServerEndpoint @ServerEndpoint} classes. * - *

For {@link javax.websocket.Endpoint}, see {@link EndpointExporter}. + *

For {@link javax.websocket.Endpoint}, see {@link ServerEndpointExporter}. * * @author Rossen Stoyanchev * @since 4.0 @@ -56,7 +56,7 @@ public class SpringConfigurator extends Configurator { } return wac.getAutowireCapableBeanFactory().createBean(endpointClass); } - if (beans.size() == 1) { + else if (beans.size() == 1) { if (logger.isTraceEnabled()) { logger.trace("Using @ServerEndpoint singleton " + beans.keySet().iterator().next()); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/Test.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/Test.java new file mode 100644 index 0000000000..a8d9cba7cd --- /dev/null +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/Test.java @@ -0,0 +1,6 @@ +package org.springframework.web.socket.server.endpoint; + + +public class Test { + +} diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/package-info.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/package-info.java index 5b5a29efbe..9192d0b16d 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/package-info.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/endpoint/package-info.java @@ -16,8 +16,8 @@ /** * Server classes for use with standard Java WebSocket endpoints including - * {@link org.springframework.web.socket.server.endpoint.EndpointRegistration} and - * {@link org.springframework.web.socket.server.endpoint.EndpointExporter} for + * {@link org.springframework.web.socket.server.endpoint.ServerEndpointRegistration} and + * {@link org.springframework.web.socket.server.endpoint.ServerEndpointExporter} for * registering type-based endpoints, * {@link org.springframework.web.socket.server.endpoint.SpringConfigurator} for * instantiating annotated endpoints through Spring. diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/GlassFishRequestUpgradeStrategy.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/GlassFishRequestUpgradeStrategy.java index efda9724ef..36175459b1 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/GlassFishRequestUpgradeStrategy.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/GlassFishRequestUpgradeStrategy.java @@ -50,7 +50,7 @@ import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.socket.server.HandshakeFailureException; -import org.springframework.web.socket.server.endpoint.EndpointRegistration; +import org.springframework.web.socket.server.endpoint.ServerEndpointRegistration; /** * GlassFish support for upgrading an {@link HttpServletRequest} during a WebSocket @@ -136,7 +136,7 @@ public class GlassFishRequestUpgradeStrategy extends AbstractEndpointUpgradeStra String randomValue = String.valueOf(random.nextLong()); String endpointPath = requestUri.endsWith("/") ? requestUri + randomValue : requestUri + "/" + randomValue; - EndpointRegistration endpointConfig = new EndpointRegistration(endpointPath, endpoint); + ServerEndpointRegistration endpointConfig = new ServerEndpointRegistration(endpointPath, endpoint); endpointConfig.setSubprotocols(Arrays.asList(selectedProtocol)); return new TyrusEndpoint(new EndpointWrapper(endpoint, endpointConfig, diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/TomcatRequestUpgradeStrategy.java b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/TomcatRequestUpgradeStrategy.java index b14cc74e20..9cdde94f19 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/server/support/TomcatRequestUpgradeStrategy.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/server/support/TomcatRequestUpgradeStrategy.java @@ -34,7 +34,7 @@ import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.web.socket.server.HandshakeFailureException; -import org.springframework.web.socket.server.endpoint.EndpointRegistration; +import org.springframework.web.socket.server.endpoint.ServerEndpointRegistration; /** * Tomcat support for upgrading an {@link HttpServletRequest} during a WebSocket handshake. @@ -77,7 +77,7 @@ public class TomcatRequestUpgradeStrategy extends AbstractEndpointUpgradeStrateg // TODO: use ServletContext attribute when Tomcat is updated WsServerContainer serverContainer = WsServerContainer.getServerContainer(); - ServerEndpointConfig endpointConfig = new EndpointRegistration("/shouldntmatter", endpoint); + ServerEndpointConfig endpointConfig = new ServerEndpointRegistration("/shouldntmatter", endpoint); upgradeHandler.preInit(endpoint, endpointConfig, serverContainer, webSocketRequest, selectedProtocol, Collections. emptyMap(), servletRequest.isSecure()); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractServerSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractServerSockJsSession.java deleted file mode 100644 index e401b4bfec..0000000000 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractServerSockJsSession.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2002-2013 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 org.springframework.web.socket.sockjs; - -import java.io.EOFException; -import java.io.IOException; -import java.net.SocketException; -import java.util.Date; -import java.util.concurrent.ScheduledFuture; - -import org.springframework.util.Assert; -import org.springframework.web.socket.CloseStatus; -import org.springframework.web.socket.TextMessage; -import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.WebSocketMessage; - -/** - * Provides partial implementations of {@link SockJsSession} methods to send messages, - * including heartbeat messages and to manage session state. - * - * @author Rossen Stoyanchev - * @since 4.0 - */ -public abstract class AbstractServerSockJsSession extends AbstractSockJsSession { - - private final SockJsConfiguration sockJsConfig; - - private ScheduledFuture heartbeatTask; - - - public AbstractServerSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) { - super(sessionId, handler); - this.sockJsConfig = config; - } - - protected SockJsConfiguration getSockJsConfig() { - return this.sockJsConfig; - } - - @Override - public final synchronized void sendMessage(WebSocketMessage message) throws IOException { - Assert.isTrue(!isClosed(), "Cannot send a message when session is closed"); - Assert.isInstanceOf(TextMessage.class, message, "Expected text message: " + message); - sendMessageInternal(((TextMessage) message).getPayload()); - } - - protected abstract void sendMessageInternal(String message) throws IOException; - - - @Override - public void connectionClosedInternal(CloseStatus status) { - updateLastActiveTime(); - cancelHeartbeat(); - } - - @Override - public final synchronized void closeInternal(CloseStatus status) throws IOException { - if (isActive()) { - // TODO: deliver messages "in flight" before sending close frame - try { - // bypass writeFrame - writeFrameInternal(SockJsFrame.closeFrame(status.getCode(), status.getReason())); - } - catch (Throwable ex) { - logger.warn("Failed to send SockJS close frame: " + ex.getMessage()); - } - } - updateLastActiveTime(); - cancelHeartbeat(); - disconnect(status); - } - - protected abstract void disconnect(CloseStatus status) throws IOException; - - /** - * For internal use within a TransportHandler and the (TransportHandler-specific) - * session sub-class. - */ - protected void writeFrame(SockJsFrame frame) throws IOException { - if (logger.isTraceEnabled()) { - logger.trace("Preparing to write " + frame); - } - try { - writeFrameInternal(frame); - } - catch (IOException ex) { - if (ex instanceof EOFException || ex instanceof SocketException) { - logger.warn("Client went away. Terminating connection"); - } - else { - logger.warn("Terminating connection due to failure to send message: " + ex.getMessage()); - } - disconnect(CloseStatus.SERVER_ERROR); - close(CloseStatus.SERVER_ERROR); - throw ex; - } - catch (Throwable ex) { - logger.warn("Terminating connection due to failure to send message: " + ex.getMessage()); - disconnect(CloseStatus.SERVER_ERROR); - close(CloseStatus.SERVER_ERROR); - throw new SockJsRuntimeException("Failed to write " + frame, ex); - } - } - - protected abstract void writeFrameInternal(SockJsFrame frame) throws Exception; - - public synchronized void sendHeartbeat() throws Exception { - if (isActive()) { - writeFrame(SockJsFrame.heartbeatFrame()); - scheduleHeartbeat(); - } - } - - protected void scheduleHeartbeat() { - Assert.notNull(getSockJsConfig().getTaskScheduler(), "heartbeatScheduler not configured"); - cancelHeartbeat(); - if (!isActive()) { - return; - } - Date time = new Date(System.currentTimeMillis() + getSockJsConfig().getHeartbeatTime()); - this.heartbeatTask = getSockJsConfig().getTaskScheduler().schedule(new Runnable() { - @Override - public void run() { - try { - sendHeartbeat(); - } - catch (Throwable t) { - // ignore - } - } - }, time); - if (logger.isTraceEnabled()) { - logger.trace("Scheduled heartbeat after " + getSockJsConfig().getHeartbeatTime()/1000 + " seconds"); - } - } - - protected void cancelHeartbeat() { - if ((this.heartbeatTask != null) && !this.heartbeatTask.isDone()) { - if (logger.isTraceEnabled()) { - logger.trace("Cancelling heartbeat"); - } - this.heartbeatTask.cancel(false); - } - this.heartbeatTask = null; - } - - -} diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsService.java index ea8089ccd1..cded84add1 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsService.java @@ -332,6 +332,7 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf return path.substring(index + prefix.length()); } } + return null; } // SockJS info request? @@ -519,5 +520,4 @@ public abstract class AbstractSockJsService implements SockJsService, SockJsConf } }; - } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsSession.java index 571dc8ea58..bb38f4ef5c 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/AbstractSockJsSession.java @@ -16,9 +16,13 @@ package org.springframework.web.socket.sockjs; +import java.io.EOFException; import java.io.IOException; +import java.net.SocketException; import java.net.URI; import java.security.Principal; +import java.util.Date; +import java.util.concurrent.ScheduledFuture; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -26,6 +30,7 @@ import org.springframework.util.Assert; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.WebSocketMessage; import org.springframework.web.socket.adapter.ConfigurableWebSocketSession; @@ -50,24 +55,29 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess private Principal principal; + private final SockJsConfiguration sockJsConfig; + private WebSocketHandler handler; private State state = State.NEW; private long timeCreated = System.currentTimeMillis(); - private long timeLastActive = System.currentTimeMillis(); + private long timeLastActive = timeCreated; + + private ScheduledFuture heartbeatTask; /** * @param sessionId * @param webSocketHandler the recipient of SockJS messages */ - public AbstractSockJsSession(String sessionId, WebSocketHandler webSocketHandler) { + public AbstractSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler webSocketHandler) { Assert.notNull(sessionId, "sessionId is required"); Assert.notNull(webSocketHandler, "webSocketHandler is required"); this.id = sessionId; this.handler = webSocketHandler; + this.sockJsConfig = config; } @Override @@ -120,6 +130,10 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess this.principal = principal; } + public SockJsConfiguration getSockJsConfig() { + return this.sockJsConfig; + } + public boolean isNew() { return State.NEW.equals(this.state); } @@ -167,35 +181,12 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess this.handler.afterConnectionEstablished(this); } - /** - * Close due to error arising from SockJS transport handling. - */ - protected void tryCloseWithSockJsTransportError(Throwable ex, CloseStatus closeStatus) { - logger.error("Closing due to transport error for " + this, ex); - try { - delegateError(ex); - } - catch (Throwable delegateEx) { - logger.error("Unhandled error for " + this, delegateEx); - try { - close(closeStatus); - } - catch (Throwable closeEx) { - logger.error("Unhandled error for " + this, closeEx); - } - } - } - public void delegateMessages(String[] messages) throws Exception { for (String message : messages) { this.handler.handleMessage(this, new TextMessage(message)); } } - public void delegateError(Throwable ex) throws Exception { - this.handler.handleTransportError(this, ex); - } - /** * Invoked in reaction to the underlying connection being closed by the remote side * (or the WebSocket container) in order to perform cleanup and notify the @@ -208,7 +199,8 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess logger.debug(this + " was closed, " + status); } try { - connectionClosedInternal(status); + updateLastActiveTime(); + cancelHeartbeat(); } finally { this.state = State.CLOSED; @@ -217,9 +209,18 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess } } - protected void connectionClosedInternal(CloseStatus status) { + public void delegateError(Throwable ex) throws Exception { + this.handler.handleTransportError(this, ex); + } + + public final synchronized void sendMessage(WebSocketMessage message) throws IOException { + Assert.isTrue(!isClosed(), "Cannot send a message when session is closed"); + Assert.isInstanceOf(TextMessage.class, message, "Expected text message: " + message); + sendMessageInternal(((TextMessage) message).getPayload()); } + protected abstract void sendMessageInternal(String message) throws IOException; + /** * {@inheritDoc} *

Performs cleanup and notifies the {@link SockJsHandler}. @@ -240,7 +241,19 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess logger.debug("Closing " + this + ", " + status); } try { - closeInternal(status); + if (isActive()) { + // TODO: deliver messages "in flight" before sending close frame + try { + // bypass writeFrame + writeFrameInternal(SockJsFrame.closeFrame(status.getCode(), status.getReason())); + } + catch (Throwable ex) { + logger.warn("Failed to send SockJS close frame: " + ex.getMessage()); + } + } + updateLastActiveTime(); + cancelHeartbeat(); + disconnect(status); } finally { this.state = State.CLOSED; @@ -254,7 +267,97 @@ public abstract class AbstractSockJsSession implements ConfigurableWebSocketSess } } - protected abstract void closeInternal(CloseStatus status) throws IOException; + protected abstract void disconnect(CloseStatus status) throws IOException; + + /** + * Close due to error arising from SockJS transport handling. + */ + protected void tryCloseWithSockJsTransportError(Throwable ex, CloseStatus closeStatus) { + logger.error("Closing due to transport error for " + this, ex); + try { + delegateError(ex); + } + catch (Throwable delegateEx) { + logger.error("Unhandled error for " + this, delegateEx); + try { + close(closeStatus); + } + catch (Throwable closeEx) { + logger.error("Unhandled error for " + this, closeEx); + } + } + } + + /** + * For internal use within a TransportHandler and the (TransportHandler-specific) + * session sub-class. + */ + protected void writeFrame(SockJsFrame frame) throws IOException { + if (logger.isTraceEnabled()) { + logger.trace("Preparing to write " + frame); + } + try { + writeFrameInternal(frame); + } + catch (IOException ex) { + if (ex instanceof EOFException || ex instanceof SocketException) { + logger.warn("Client went away. Terminating connection"); + } + else { + logger.warn("Terminating connection due to failure to send message: " + ex.getMessage()); + } + disconnect(CloseStatus.SERVER_ERROR); + close(CloseStatus.SERVER_ERROR); + throw ex; + } + catch (Throwable ex) { + logger.warn("Terminating connection due to failure to send message: " + ex.getMessage()); + disconnect(CloseStatus.SERVER_ERROR); + close(CloseStatus.SERVER_ERROR); + throw new SockJsRuntimeException("Failed to write " + frame, ex); + } + } + + protected abstract void writeFrameInternal(SockJsFrame frame) throws Exception; + + public synchronized void sendHeartbeat() throws Exception { + if (isActive()) { + writeFrame(SockJsFrame.heartbeatFrame()); + scheduleHeartbeat(); + } + } + + protected void scheduleHeartbeat() { + Assert.notNull(this.sockJsConfig.getTaskScheduler(), "heartbeatScheduler not configured"); + cancelHeartbeat(); + if (!isActive()) { + return; + } + Date time = new Date(System.currentTimeMillis() + this.sockJsConfig.getHeartbeatTime()); + this.heartbeatTask = this.sockJsConfig.getTaskScheduler().schedule(new Runnable() { + public void run() { + try { + sendHeartbeat(); + } + catch (Throwable t) { + // ignore + } + } + }, time); + if (logger.isTraceEnabled()) { + logger.trace("Scheduled heartbeat after " + this.sockJsConfig.getHeartbeatTime()/1000 + " seconds"); + } + } + + protected void cancelHeartbeat() { + if ((this.heartbeatTask != null) && !this.heartbeatTask.isDone()) { + if (logger.isTraceEnabled()) { + logger.trace("Cancelling heartbeat"); + } + this.heartbeatTask.cancel(false); + } + this.heartbeatTask = null; + } @Override diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsFrame.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsFrame.java index d70a500232..73c4d710f9 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsFrame.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/SockJsFrame.java @@ -41,6 +41,7 @@ public class SockJsFrame { private SockJsFrame(String content) { + Assert.notNull("content is required"); this.content = content; } @@ -116,6 +117,22 @@ public class SockJsFrame { return "SockJsFrame content='" + result.replace("\n", "\\n").replace("\r", "\\r") + "'"; } + @Override + public int hashCode() { + return this.content.hashCode(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof SockJsFrame)) { + return false; + } + return this.content.equals(((SockJsFrame) other).content); + } + private static class MessageFrame extends SockJsFrame { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/TransportType.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/TransportType.java index 1c2c5814d7..98f6316b54 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/TransportType.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/TransportType.java @@ -79,7 +79,7 @@ public enum TransportType { return this.httpMethod; } - public boolean setsNoCache() { + public boolean sendsNoCacheInstruction() { return this.headerHints.contains("no_cache"); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/DefaultSockJsService.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/DefaultSockJsService.java index 48d25224d9..5d10d461e4 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/DefaultSockJsService.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/support/DefaultSockJsService.java @@ -35,6 +35,7 @@ import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.scheduling.TaskScheduler; import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.DefaultHandshakeHandler; import org.springframework.web.socket.server.HandshakeHandler; @@ -84,7 +85,8 @@ public class DefaultSockJsService extends AbstractSockJsService { * application stops. */ public DefaultSockJsService(TaskScheduler taskScheduler) { - this(taskScheduler, null); + super(taskScheduler); + addTransportHandlers(getDefaultTransportHandlers()); } /** @@ -105,9 +107,16 @@ public class DefaultSockJsService extends AbstractSockJsService { super(taskScheduler); - transportHandlers = CollectionUtils.isEmpty(transportHandlers) ? getDefaultTransportHandlers() : transportHandlers; - addTransportHandlers(transportHandlers); - addTransportHandlers(Arrays.asList(transportHandlerOverrides)); + if (!CollectionUtils.isEmpty(transportHandlers)) { + addTransportHandlers(transportHandlers); + } + if (!ObjectUtils.isEmpty(transportHandlerOverrides)) { + addTransportHandlers(Arrays.asList(transportHandlerOverrides)); + } + + if (this.transportHandlers.isEmpty()) { + logger.warn("No transport handlers"); + } } protected final Set getDefaultTransportHandlers() { @@ -194,7 +203,7 @@ public class DefaultSockJsService extends AbstractSockJsService { transportHandler, request, response); if (session != null) { - if (transportType.setsNoCache()) { + if (transportType.sendsNoCacheInstruction()) { addNoCacheHeaders(response); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpReceivingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpReceivingTransportHandler.java index 237343c9ac..4dfc20adc1 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpReceivingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpReceivingTransportHandler.java @@ -28,8 +28,6 @@ import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.sockjs.AbstractSockJsSession; -import org.springframework.web.socket.sockjs.SockJsFrame; -import org.springframework.web.socket.sockjs.SockJsRuntimeException; import org.springframework.web.socket.sockjs.TransportErrorException; import org.springframework.web.socket.sockjs.TransportHandler; import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator; @@ -38,7 +36,7 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; /** - * TODO + * Base class for HTTP-based transports that read input messages. * * @author Rossen Stoyanchev * @since 4.0 @@ -57,8 +55,7 @@ public abstract class AbstractHttpReceivingTransportHandler implements Transport @Override public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response, - WebSocketHandler webSocketHandler, AbstractSockJsSession session) - throws TransportErrorException { + WebSocketHandler webSocketHandler, AbstractSockJsSession session) throws TransportErrorException { if (session == null) { response.setStatusCode(HttpStatus.NOT_FOUND); @@ -77,21 +74,26 @@ public abstract class AbstractHttpReceivingTransportHandler implements Transport messages = readMessages(request); } catch (JsonMappingException ex) { - logger.error("Failed to read message: ", ex); + logger.error("Failed to read message: " + ex.getMessage()); sendInternalServerError(response, "Payload expected.", session.getId()); return; } catch (IOException ex) { - logger.error("Failed to read message: ", ex); + logger.error("Failed to read message: " + ex.getMessage()); sendInternalServerError(response, "Broken JSON encoding.", session.getId()); return; } catch (Throwable t) { - logger.error("Failed to read message: ", t); + logger.error("Failed to read message: " + t.getMessage()); sendInternalServerError(response, "Failed to process messages", session.getId()); return; } + if (messages == null) { + sendInternalServerError(response, "Payload expected.", session.getId()); + return; + } + if (logger.isTraceEnabled()) { logger.trace("Received message(s): " + Arrays.asList(messages)); } @@ -104,7 +106,7 @@ public abstract class AbstractHttpReceivingTransportHandler implements Transport } catch (Throwable t) { ExceptionWebSocketHandlerDecorator.tryCloseWithError(session, t, logger); - throw new SockJsRuntimeException("Unhandled WebSocketHandler error in " + this, t); + throw new TransportErrorException("Unhandled WebSocketHandler error in " + this, t, session.getId()); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSendingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSendingTransportHandler.java index 8c76b8c14e..9d91ce0231 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSendingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSendingTransportHandler.java @@ -28,9 +28,9 @@ import org.springframework.web.socket.sockjs.AbstractSockJsSession; import org.springframework.web.socket.sockjs.ConfigurableTransportHandler; import org.springframework.web.socket.sockjs.SockJsConfiguration; import org.springframework.web.socket.sockjs.SockJsFrame; +import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat; import org.springframework.web.socket.sockjs.SockJsSessionFactory; import org.springframework.web.socket.sockjs.TransportErrorException; -import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat; /** * TODO @@ -57,18 +57,17 @@ public abstract class AbstractHttpSendingTransportHandler @Override public final void handleRequest(ServerHttpRequest request, ServerHttpResponse response, - WebSocketHandler webSocketHandler, AbstractSockJsSession session) - throws TransportErrorException { + WebSocketHandler webSocketHandler, AbstractSockJsSession session) throws TransportErrorException { // Set content type before writing response.getHeaders().setContentType(getContentType()); - AbstractHttpServerSockJsSession httpServerSession = (AbstractHttpServerSockJsSession) session; + AbstractHttpSockJsSession httpServerSession = (AbstractHttpSockJsSession) session; handleRequestInternal(request, response, httpServerSession); } protected void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response, - AbstractHttpServerSockJsSession httpServerSession) throws TransportErrorException { + AbstractHttpSockJsSession httpServerSession) throws TransportErrorException { if (httpServerSession.isNew()) { logger.debug("Opening " + getTransportType() + " connection"); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpServerSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSockJsSession.java similarity index 95% rename from spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpServerSockJsSession.java rename to spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSockJsSession.java index 51b4bd4292..85423572f0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpServerSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSockJsSession.java @@ -26,11 +26,11 @@ import org.springframework.http.server.ServerHttpResponse; import org.springframework.util.Assert; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.WebSocketHandler; -import org.springframework.web.socket.sockjs.AbstractServerSockJsSession; +import org.springframework.web.socket.sockjs.AbstractSockJsSession; import org.springframework.web.socket.sockjs.SockJsConfiguration; import org.springframework.web.socket.sockjs.SockJsFrame; -import org.springframework.web.socket.sockjs.TransportErrorException; import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat; +import org.springframework.web.socket.sockjs.TransportErrorException; import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator; /** @@ -39,7 +39,7 @@ import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator * @author Rossen Stoyanchev * @since 4.0 */ -public abstract class AbstractHttpServerSockJsSession extends AbstractServerSockJsSession { +public abstract class AbstractHttpSockJsSession extends AbstractSockJsSession { private FrameFormat frameFormat; @@ -50,7 +50,7 @@ public abstract class AbstractHttpServerSockJsSession extends AbstractServerSock private ServerHttpResponse response; - public AbstractHttpServerSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) { + public AbstractHttpSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) { super(sessionId, config, handler); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/EventSourceTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/EventSourceTransportHandler.java index f469628c05..2556c62b88 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/EventSourceTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/EventSourceTransportHandler.java @@ -46,9 +46,9 @@ public class EventSourceTransportHandler extends AbstractHttpSendingTransportHan } @Override - public StreamingServerSockJsSession createSession(String sessionId, WebSocketHandler handler) { + public StreamingSockJsSession createSession(String sessionId, WebSocketHandler handler) { Assert.notNull(getSockJsConfig(), "This transport requires SockJsConfiguration"); - return new StreamingServerSockJsSession(sessionId, getSockJsConfig(), handler) { + return new StreamingSockJsSession(sessionId, getSockJsConfig(), handler) { @Override protected void writePrelude() throws IOException { getResponse().getBody().write('\r'); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/HtmlFileTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/HtmlFileTransportHandler.java index acf2c877fc..752fcfcbc0 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/HtmlFileTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/HtmlFileTransportHandler.java @@ -80,10 +80,10 @@ public class HtmlFileTransportHandler extends AbstractHttpSendingTransportHandle } @Override - public StreamingServerSockJsSession createSession(String sessionId, WebSocketHandler handler) { + public StreamingSockJsSession createSession(String sessionId, WebSocketHandler handler) { Assert.notNull(getSockJsConfig(), "This transport requires SockJsConfiguration"); - return new StreamingServerSockJsSession(sessionId, getSockJsConfig(), handler) { + return new StreamingSockJsSession(sessionId, getSockJsConfig(), handler) { @Override protected void writePrelude() throws IOException { @@ -99,7 +99,7 @@ public class HtmlFileTransportHandler extends AbstractHttpSendingTransportHandle @Override public void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response, - AbstractHttpServerSockJsSession session) throws TransportErrorException { + AbstractHttpSockJsSession session) throws TransportErrorException { try { String callback = request.getQueryParams().getFirst("c"); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpPollingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpPollingTransportHandler.java index e155c3a5ac..a3588bf51a 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpPollingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpPollingTransportHandler.java @@ -50,14 +50,14 @@ public class JsonpPollingTransportHandler extends AbstractHttpSendingTransportHa } @Override - public PollingServerSockJsSession createSession(String sessionId, WebSocketHandler handler) { + public PollingSockJsSession createSession(String sessionId, WebSocketHandler handler) { Assert.notNull(getSockJsConfig(), "This transport requires SockJsConfiguration"); - return new PollingServerSockJsSession(sessionId, getSockJsConfig(), handler); + return new PollingSockJsSession(sessionId, getSockJsConfig(), handler); } @Override public void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response, - AbstractHttpServerSockJsSession session) throws TransportErrorException { + AbstractHttpSockJsSession session) throws TransportErrorException { try { String callback = request.getQueryParams().getFirst("c"); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpTransportHandler.java index 6cedeea2c0..3f64dd3c56 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/JsonpTransportHandler.java @@ -20,14 +20,20 @@ import java.io.IOException; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; import org.springframework.web.socket.sockjs.AbstractSockJsSession; import org.springframework.web.socket.sockjs.TransportErrorException; import org.springframework.web.socket.sockjs.TransportType; public class JsonpTransportHandler extends AbstractHttpReceivingTransportHandler { + private final FormHttpMessageConverter formConverter = new FormHttpMessageConverter(); + + @Override public TransportType getTransportType() { return TransportType.JSONP_SEND; @@ -37,13 +43,6 @@ public class JsonpTransportHandler extends AbstractHttpReceivingTransportHandler public void handleRequestInternal(ServerHttpRequest request, ServerHttpResponse response, AbstractSockJsSession sockJsSession) throws TransportErrorException { - if (MediaType.APPLICATION_FORM_URLENCODED.equals(request.getHeaders().getContentType())) { - if (request.getQueryParams().getFirst("d") == null) { - sendInternalServerError(response, "Payload expected.", sockJsSession.getId()); - return; - } - } - super.handleRequestInternal(request, response, sockJsSession); try { @@ -57,8 +56,9 @@ public class JsonpTransportHandler extends AbstractHttpReceivingTransportHandler @Override protected String[] readMessages(ServerHttpRequest request) throws IOException { if (MediaType.APPLICATION_FORM_URLENCODED.equals(request.getHeaders().getContentType())) { - String d = request.getQueryParams().getFirst("d"); - return getObjectMapper().readValue(d, String[].class); + MultiValueMap map = this.formConverter.read(null, request); + String d = map.getFirst("d"); + return (StringUtils.hasText(d)) ? getObjectMapper().readValue(d, String[].class) : null; } else { return getObjectMapper().readValue(request.getBody(), String[].class); diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/PollingServerSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/PollingSockJsSession.java similarity index 87% rename from spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/PollingServerSockJsSession.java rename to spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/PollingSockJsSession.java index 75e6945089..5d3f9d0b16 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/PollingServerSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/PollingSockJsSession.java @@ -22,9 +22,9 @@ import org.springframework.web.socket.sockjs.SockJsConfiguration; import org.springframework.web.socket.sockjs.SockJsFrame; -public class PollingServerSockJsSession extends AbstractHttpServerSockJsSession { +public class PollingSockJsSession extends AbstractHttpSockJsSession { - public PollingServerSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) { + public PollingSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) { super(sessionId, config, handler); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/StreamingServerSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/StreamingSockJsSession.java similarity index 93% rename from spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/StreamingServerSockJsSession.java rename to spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/StreamingSockJsSession.java index c0f176b891..bb39624444 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/StreamingServerSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/StreamingSockJsSession.java @@ -26,12 +26,12 @@ import org.springframework.web.socket.sockjs.SockJsFrame; import org.springframework.web.socket.sockjs.TransportErrorException; import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat; -public class StreamingServerSockJsSession extends AbstractHttpServerSockJsSession { +public class StreamingSockJsSession extends AbstractHttpSockJsSession { private int byteCount; - public StreamingServerSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) { + public StreamingSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) { super(sessionId, config, handler); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSession.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSession.java index 48bd272d7e..44b0bec5e7 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSession.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSession.java @@ -23,7 +23,7 @@ import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; -import org.springframework.web.socket.sockjs.AbstractServerSockJsSession; +import org.springframework.web.socket.sockjs.AbstractSockJsSession; import org.springframework.web.socket.sockjs.SockJsConfiguration; import org.springframework.web.socket.sockjs.SockJsFrame; @@ -31,10 +31,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; /** + * A WebSocket implementation of {@link AbstractSockJsSession}. Delegates to a + * {@link WebSocketSession}. + * * @author Rossen Stoyanchev * @since 4.0 */ -public class WebSocketServerSockJsSession extends AbstractServerSockJsSession { +public class WebSocketServerSockJsSession extends AbstractSockJsSession { private WebSocketSession webSocketSession; @@ -103,5 +106,5 @@ public class WebSocketServerSockJsSession extends AbstractServerSockJsSession { protected void disconnect(CloseStatus status) throws IOException { this.webSocketSession.close(status); } -} +} diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrPollingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrPollingTransportHandler.java index 1c5bc97f8d..b3629fe194 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrPollingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrPollingTransportHandler.java @@ -51,9 +51,9 @@ public class XhrPollingTransportHandler extends AbstractHttpSendingTransportHand } @Override - public PollingServerSockJsSession createSession(String sessionId, WebSocketHandler handler) { + public PollingSockJsSession createSession(String sessionId, WebSocketHandler handler) { Assert.notNull(getSockJsConfig(), "This transport requires SockJsConfiguration"); - return new PollingServerSockJsSession(sessionId, getSockJsConfig(), handler); + return new PollingSockJsSession(sessionId, getSockJsConfig(), handler); } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrStreamingTransportHandler.java b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrStreamingTransportHandler.java index f99f27bdf3..7083d17937 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrStreamingTransportHandler.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/sockjs/transport/XhrStreamingTransportHandler.java @@ -47,10 +47,10 @@ public class XhrStreamingTransportHandler extends AbstractHttpSendingTransportHa } @Override - public StreamingServerSockJsSession createSession(String sessionId, WebSocketHandler handler) { + public StreamingSockJsSession createSession(String sessionId, WebSocketHandler handler) { Assert.notNull(getSockJsConfig(), "This transport requires SockJsConfiguration"); - return new StreamingServerSockJsSession(sessionId, getSockJsConfig(), handler) { + return new StreamingSockJsSession(sessionId, getSockJsConfig(), handler) { @Override protected void writePrelude() throws IOException { diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/support/WebSocketHandlerDecorator.java b/spring-websocket/src/main/java/org/springframework/web/socket/support/WebSocketHandlerDecorator.java index 96ab730434..00c5dcdbcc 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/support/WebSocketHandlerDecorator.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/support/WebSocketHandlerDecorator.java @@ -38,7 +38,7 @@ public class WebSocketHandlerDecorator implements WebSocketHandler { } - protected WebSocketHandler getDelegate() { + public WebSocketHandler getDelegate() { return this.delegate; } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/AbstractHttpRequestTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/AbstractHttpRequestTests.java index 7ef451307e..2965f4ddbf 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/AbstractHttpRequestTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/AbstractHttpRequestTests.java @@ -41,18 +41,21 @@ public class AbstractHttpRequestTests { @Before public void setUp() { - this.servletRequest = new MockHttpServletRequest(); - this.servletResponse = new MockHttpServletResponse(); - this.request = new AsyncServletServerHttpRequest(this.servletRequest, this.servletResponse); - this.response = new ServletServerHttpResponse(this.servletResponse); + resetRequestAndResponse(); } - protected void setRequest(String method, String requestUri) { this.servletRequest.setMethod(method); this.servletRequest.setRequestURI(requestUri); } + protected void resetRequestAndResponse() { + resetResponse(); + this.servletRequest = new MockHttpServletRequest(); + this.servletRequest.setAsyncSupported(true); + this.request = new AsyncServletServerHttpRequest(this.servletRequest, this.servletResponse); + } + protected void resetResponse() { this.servletResponse = new MockHttpServletResponse(); this.response = new ServletServerHttpResponse(this.servletResponse); diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/adapter/JettyWebSocketListenerAdapterTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/adapter/JettyWebSocketListenerAdapterTests.java new file mode 100644 index 0000000000..b13326895e --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/adapter/JettyWebSocketListenerAdapterTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.adapter; + +import org.eclipse.jetty.websocket.api.Session; +import org.junit.Before; +import org.junit.Test; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.WebSocketHandler; + +import static org.mockito.Mockito.*; + + +/** + * Test fixture for {@link JettyWebSocketListenerAdapter}. + * + * @author Rossen Stoyanchev + */ +public class JettyWebSocketListenerAdapterTests { + + private JettyWebSocketListenerAdapter adapter; + + private WebSocketHandler webSocketHandler; + + private JettyWebSocketSessionAdapter webSocketSession; + + private Session session; + + + @Before + public void setup() { + this.session = mock(Session.class); + this.webSocketHandler = mock(WebSocketHandler.class); + this.webSocketSession = new JettyWebSocketSessionAdapter(); + this.adapter = new JettyWebSocketListenerAdapter(this.webSocketHandler, this.webSocketSession); + } + + @Test + public void onOpen() throws Throwable { + this.adapter.onWebSocketConnect(this.session); + verify(this.webSocketHandler).afterConnectionEstablished(this.webSocketSession); + } + + @Test + public void onClose() throws Throwable { + this.adapter.onWebSocketClose(1000, "reason"); + verify(this.webSocketHandler).afterConnectionClosed(this.webSocketSession, CloseStatus.NORMAL.withReason("reason")); + } + + @Test + public void onError() throws Throwable { + Exception exception = new Exception(); + this.adapter.onWebSocketError(exception); + verify(this.webSocketHandler).handleTransportError(this.webSocketSession, exception); + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/adapter/StandardEndpointAdapterTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/adapter/StandardEndpointAdapterTests.java new file mode 100644 index 0000000000..4c9c64fe43 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/adapter/StandardEndpointAdapterTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.adapter; + +import javax.websocket.CloseReason; +import javax.websocket.CloseReason.CloseCodes; +import javax.websocket.MessageHandler; +import javax.websocket.Session; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.WebSocketHandler; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + + +/** + * Test fixture for {@link StandardEndpointAdapter}. + * + * @author Rossen Stoyanchev + */ +public class StandardEndpointAdapterTests { + + private StandardEndpointAdapter adapter; + + private WebSocketHandler webSocketHandler; + + private StandardWebSocketSessionAdapter webSocketSession; + + private Session session; + + + @Before + public void setup() { + this.session = mock(Session.class); + this.webSocketHandler = mock(WebSocketHandler.class); + this.webSocketSession = new StandardWebSocketSessionAdapter(); + this.adapter = new StandardEndpointAdapter(webSocketHandler, webSocketSession); + } + + @Test + public void onOpen() throws Throwable { + this.adapter.onOpen(session, null); + + verify(this.webSocketHandler).afterConnectionEstablished(this.webSocketSession); + verify(session, atLeast(2)).addMessageHandler(any(MessageHandler.Whole.class)); + + when(session.getId()).thenReturn("123"); + assertEquals("123", this.webSocketSession.getId()); + } + + @Test + public void onClose() throws Throwable { + this.adapter.onClose(session, new CloseReason(CloseCodes.NORMAL_CLOSURE, "reason")); + verify(this.webSocketHandler).afterConnectionClosed(this.webSocketSession, CloseStatus.NORMAL.withReason("reason")); + } + + @Test + public void onError() throws Throwable { + Exception exception = new Exception(); + this.adapter.onError(session, exception); + verify(this.webSocketHandler).handleTransportError(this.webSocketSession, exception); + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/client/WebSocketConnectionManagerTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/client/WebSocketConnectionManagerTests.java new file mode 100644 index 0000000000..118ef0d40a --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/client/WebSocketConnectionManagerTests.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.client; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.context.SmartLifecycle; +import org.springframework.http.HttpHeaders; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.adapter.WebSocketHandlerAdapter; +import org.springframework.web.socket.support.ExceptionWebSocketHandlerDecorator; +import org.springframework.web.socket.support.LoggingWebSocketHandlerDecorator; +import org.springframework.web.socket.support.WebSocketHandlerDecorator; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + + +/** + * Test fixture for {@link WebSocketConnectionManager}. + * + * @author Rossen Stoyanchev + */ +public class WebSocketConnectionManagerTests { + + + @Test + public void openConnection() throws Exception { + + List subprotocols = Arrays.asList("abc"); + HttpHeaders headers = new HttpHeaders(); + headers.setSecWebSocketProtocol(subprotocols); + + WebSocketClient client = mock(WebSocketClient.class); + WebSocketHandler handler = new WebSocketHandlerAdapter(); + + WebSocketConnectionManager manager = new WebSocketConnectionManager(client, handler , "/path/{id}", "123"); + manager.setSubProtocols(subprotocols); + manager.openConnection(); + + ArgumentCaptor captor = ArgumentCaptor.forClass(WebSocketHandlerDecorator.class); + ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(HttpHeaders.class); + ArgumentCaptor uriCaptor = ArgumentCaptor.forClass(URI.class); + + verify(client).doHandshake(captor.capture(), headersCaptor.capture(), uriCaptor.capture()); + + assertEquals(headers, headersCaptor.getValue()); + assertEquals(new URI("/path/123"), uriCaptor.getValue()); + + WebSocketHandlerDecorator loggingHandler = captor.getValue(); + assertEquals(LoggingWebSocketHandlerDecorator.class, loggingHandler.getClass()); + + WebSocketHandlerDecorator exceptionHandler = (WebSocketHandlerDecorator) loggingHandler.getDelegate(); + assertNotNull(exceptionHandler); + assertEquals(ExceptionWebSocketHandlerDecorator.class, exceptionHandler.getClass()); + + assertSame(handler, exceptionHandler.getDelegate()); + } + + @Test + public void syncClientLifecycle() throws Exception { + + TestLifecycleWebSocketClient client = new TestLifecycleWebSocketClient(false); + WebSocketHandler handler = new WebSocketHandlerAdapter(); + WebSocketConnectionManager manager = new WebSocketConnectionManager(client, handler , "/a"); + + manager.startInternal(); + assertTrue(client.isRunning()); + + manager.stopInternal(); + assertFalse(client.isRunning()); + } + + @Test + public void dontSyncClientLifecycle() throws Exception { + + TestLifecycleWebSocketClient client = new TestLifecycleWebSocketClient(true); + WebSocketHandler handler = new WebSocketHandlerAdapter(); + WebSocketConnectionManager manager = new WebSocketConnectionManager(client, handler , "/a"); + + manager.startInternal(); + assertTrue(client.isRunning()); + + manager.stopInternal(); + assertTrue(client.isRunning()); + } + + + private static class TestLifecycleWebSocketClient implements WebSocketClient, SmartLifecycle { + + private boolean running; + + public TestLifecycleWebSocketClient(boolean running) { + this.running = running; + } + + @Override + public void start() { + this.running = true; + } + + @Override + public void stop() { + this.running = false; + } + + @Override + public boolean isRunning() { + return this.running; + } + + @Override + public int getPhase() { + return 0; + } + + @Override + public boolean isAutoStartup() { + return false; + } + + @Override + public void stop(Runnable callback) { + this.running = false; + } + + @Override + public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, String uriTemplate, Object... uriVariables) + throws WebSocketConnectFailureException { + return null; + } + + @Override + public WebSocketSession doHandshake(WebSocketHandler webSocketHandler, HttpHeaders headers, URI uri) + throws WebSocketConnectFailureException { + return null; + } + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/client/endpoint/StandardWebSocketClientTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/client/endpoint/StandardWebSocketClientTests.java new file mode 100644 index 0000000000..54187c3991 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/client/endpoint/StandardWebSocketClientTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.client.endpoint; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.websocket.ClientEndpointConfig; +import javax.websocket.Endpoint; +import javax.websocket.WebSocketContainer; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.http.HttpHeaders; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.adapter.StandardEndpointAdapter; +import org.springframework.web.socket.adapter.WebSocketHandlerAdapter; +import org.springframework.web.socket.client.endpoint.StandardWebSocketClient; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + + +/** + * Test fixture for {@link StandardWebSocketClient}. + * + * @author Rossen Stoyanchev + */ +public class StandardWebSocketClientTests { + + + @Test + public void doHandshake() throws Exception { + + URI uri = new URI("ws://example.com/abc"); + List subprotocols = Arrays.asList("abc"); + + HttpHeaders headers = new HttpHeaders(); + headers.setSecWebSocketProtocol(subprotocols); + headers.add("foo", "bar"); + + WebSocketHandler handler = new WebSocketHandlerAdapter(); + WebSocketContainer webSocketContainer = mock(WebSocketContainer.class); + StandardWebSocketClient client = new StandardWebSocketClient(); + client.setWebSocketContainer(webSocketContainer); + WebSocketSession session = client.doHandshake(handler, headers, uri); + + ArgumentCaptor endpointArg = ArgumentCaptor.forClass(Endpoint.class); + ArgumentCaptor configArg = ArgumentCaptor.forClass(ClientEndpointConfig.class); + ArgumentCaptor uriArg = ArgumentCaptor.forClass(URI.class); + + + verify(webSocketContainer).connectToServer(endpointArg.capture(), configArg.capture(), uriArg.capture()); + + + assertNotNull(endpointArg.getValue()); + assertEquals(StandardEndpointAdapter.class, endpointArg.getValue().getClass()); + + ClientEndpointConfig config = configArg.getValue(); + assertEquals(subprotocols, config.getPreferredSubprotocols()); + + Map> map = new HashMap<>(); + config.getConfigurator().beforeRequest(map); + assertEquals(Collections.singletonMap("foo", Arrays.asList("bar")), map); + + assertEquals(uri, uriArg.getValue()); + assertEquals(uri, session.getUri()); + assertEquals("example.com", session.getRemoteHostName()); + } +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/server/endpoint/ServerEndpointExporterTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/server/endpoint/ServerEndpointExporterTests.java new file mode 100644 index 0000000000..99881546ee --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/server/endpoint/ServerEndpointExporterTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.server.endpoint; + +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.Session; +import javax.websocket.server.ServerContainer; +import javax.websocket.server.ServerEndpoint; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.web.test.MockServletContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +import static org.mockito.Mockito.*; + + +/** + * Test fixture for {@link ServerEndpointExporter}. + * + * @author Rossen Stoyanchev + */ +public class ServerEndpointExporterTests { + + private ServerContainer serverContainer; + + private ServerEndpointExporter exporter; + + private AnnotationConfigWebApplicationContext webAppContext; + + + @Before + public void setup() { + this.serverContainer = mock(ServerContainer.class); + + MockServletContext servletContext = new MockServletContext(); + servletContext.setAttribute("javax.websocket.server.ServerContainer", serverContainer); + + this.webAppContext = new AnnotationConfigWebApplicationContext(); + this.webAppContext.register(Config.class); + this.webAppContext.setServletContext(servletContext); + this.webAppContext.refresh(); + + this.exporter = new ServerEndpointExporter(); + this.exporter.setApplicationContext(this.webAppContext); + } + + + @Test + public void addAnnotatedEndpointBean() throws Exception { + + this.exporter.setAnnotatedEndpointClasses(AnnotatedDummyEndpoint.class); + this.exporter.afterPropertiesSet(); + + verify(this.serverContainer).addEndpoint(AnnotatedDummyEndpoint.class); + verify(this.serverContainer).addEndpoint(AnnotatedDummyEndpointBean.class); + } + + @Test + public void addServerEndpointConfigBean() throws Exception { + + ServerEndpointRegistration endpointRegistration = new ServerEndpointRegistration("/dummy", new DummyEndpoint()); + this.exporter.postProcessAfterInitialization(endpointRegistration, "dummyEndpoint"); + + verify(this.serverContainer).addEndpoint(endpointRegistration); + } + + + private static class DummyEndpoint extends Endpoint { + + @Override + public void onOpen(Session session, EndpointConfig config) { + } + } + + @ServerEndpoint("/path") + private static class AnnotatedDummyEndpoint { + } + + @ServerEndpoint("/path") + private static class AnnotatedDummyEndpointBean { + } + + @Configuration + static class Config { + + @Bean + public AnnotatedDummyEndpointBean annotatedEndpoint1() { + return new AnnotatedDummyEndpointBean(); + } + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/server/endpoint/ServerEndpointRegistrationTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/server/endpoint/ServerEndpointRegistrationTests.java new file mode 100644 index 0000000000..47280fecf9 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/server/endpoint/ServerEndpointRegistrationTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.server.endpoint; + +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.Session; + +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.junit.Assert.*; + + +/** + * Test fixture for {@link ServerEndpointRegistration}. + * + * @author Rossen Stoyanchev + */ +public class ServerEndpointRegistrationTests { + + + @Test + public void endpointPerConnection() throws Exception { + + @SuppressWarnings("resource") + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + + ServerEndpointRegistration registration = new ServerEndpointRegistration("/path", EchoEndpoint.class); + registration.setBeanFactory(context.getBeanFactory()); + + EchoEndpoint endpoint = registration.getConfigurator().getEndpointInstance(EchoEndpoint.class); + + assertNotNull(endpoint); + } + + @Test + public void endpointSingleton() throws Exception { + + EchoEndpoint endpoint = new EchoEndpoint(new EchoService()); + ServerEndpointRegistration registration = new ServerEndpointRegistration("/path", endpoint); + + EchoEndpoint actual = registration.getConfigurator().getEndpointInstance(EchoEndpoint.class); + + assertSame(endpoint, actual); + } + + + @Configuration + static class Config { + + @Bean + public EchoService echoService() { + return new EchoService(); + } + } + + private static class EchoEndpoint extends Endpoint { + + @SuppressWarnings("unused") + private final EchoService service; + + @Autowired + public EchoEndpoint(EchoService service) { + this.service = service; + } + + @Override + public void onOpen(Session session, EndpointConfig config) { + } + } + + private static class EchoService { } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/server/endpoint/SpringConfiguratorTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/server/endpoint/SpringConfiguratorTests.java new file mode 100644 index 0000000000..433c2c8411 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/server/endpoint/SpringConfiguratorTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.server.endpoint; + +import javax.websocket.Endpoint; +import javax.websocket.EndpointConfig; +import javax.websocket.Session; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.web.test.MockServletContext; +import org.springframework.web.context.ContextLoader; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +import static org.junit.Assert.*; + + +public class SpringConfiguratorTests { + + + private MockServletContext servletContext; + + private ContextLoader contextLoader; + + private AnnotationConfigWebApplicationContext webAppContext; + + + @Before + public void setup() { + this.servletContext = new MockServletContext(); + + this.webAppContext = new AnnotationConfigWebApplicationContext(); + this.webAppContext.register(Config.class); + + this.contextLoader = new ContextLoader(webAppContext); + this.contextLoader.initWebApplicationContext(this.servletContext); + } + + @After + public void destroy() { + this.contextLoader.closeWebApplicationContext(this.servletContext); + } + + + @Test + public void getEndpointInstanceCreateBean() throws Exception { + + PerConnectionEchoEndpoint endpoint = new SpringConfigurator().getEndpointInstance(PerConnectionEchoEndpoint.class); + + assertNotNull(endpoint); + } + + @Test + public void getEndpointInstanceUseBean() throws Exception { + + EchoEndpointBean expected = this.webAppContext.getBean(EchoEndpointBean.class); + EchoEndpointBean actual = new SpringConfigurator().getEndpointInstance(EchoEndpointBean.class); + + assertSame(expected, actual); + } + + + @Configuration + static class Config { + + @Bean + public EchoEndpointBean echoEndpointBean() { + return new EchoEndpointBean(echoService()); + } + + @Bean + public EchoService echoService() { + return new EchoService(); + } + } + + private static class EchoEndpointBean extends Endpoint { + + @SuppressWarnings("unused") + private final EchoService service; + + @Autowired + public EchoEndpointBean(EchoService service) { + this.service = service; + } + + @Override + public void onOpen(Session session, EndpointConfig config) { + } + } + + private static class PerConnectionEchoEndpoint extends Endpoint { + + @SuppressWarnings("unused") + private final EchoService service; + + @Autowired + public PerConnectionEchoEndpoint(EchoService service) { + this.service = service; + } + + @Override + public void onOpen(Session session, EndpointConfig config) { + } + } + + private static class EchoService { } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsServiceTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsServiceTests.java index 01169740e9..af75a313ac 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsServiceTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsServiceTests.java @@ -31,6 +31,8 @@ import org.springframework.web.socket.WebSocketHandler; import static org.junit.Assert.*; /** + * Test fixture for {@link AbstractSockJsService}. + * * @author Rossen Stoyanchev */ public class AbstractSockJsServiceTests extends AbstractHttpRequestTests { @@ -48,65 +50,156 @@ public class AbstractSockJsServiceTests extends AbstractHttpRequestTests { } @Test - public void getSockJsPath() throws Exception { + public void getSockJsPathForGreetingRequest() throws Exception { - handleRequest("/echo", HttpStatus.OK); + handleRequest("GET", "/a", HttpStatus.OK); assertEquals("Welcome to SockJS!\n", this.servletResponse.getContentAsString()); - handleRequest("/echo/info", HttpStatus.OK); - assertTrue(this.servletResponse.getContentAsString().startsWith("{\"entropy\":")); + handleRequest("GET", "/a/", HttpStatus.OK); + assertEquals("Welcome to SockJS!\n", this.servletResponse.getContentAsString()); + + this.service.setValidSockJsPrefixes("/b"); - handleRequest("/echo/", HttpStatus.OK); + handleRequest("GET", "/a", HttpStatus.NOT_FOUND); + handleRequest("GET", "/a/", HttpStatus.NOT_FOUND); + + handleRequest("GET", "/b", HttpStatus.OK); assertEquals("Welcome to SockJS!\n", this.servletResponse.getContentAsString()); + } - handleRequest("/echo/iframe.html", HttpStatus.OK); - assertTrue(this.servletResponse.getContentAsString().startsWith("\n")); + @Test + public void getSockJsPathForInfoRequest() throws Exception { + + handleRequest("GET", "/a/info", HttpStatus.OK); + assertTrue(this.servletResponse.getContentAsString().startsWith("{\"entropy\":")); + + this.service.setValidSockJsPrefixes("/b"); + + handleRequest("GET", "/a/info", HttpStatus.NOT_FOUND); + + handleRequest("GET", "/b/info", HttpStatus.OK); + assertTrue(this.servletResponse.getContentAsString().startsWith("{\"entropy\":")); + } + + @Test + public void getSockJsPathForTransportRequest() throws Exception { - handleRequest("/echo/websocket", HttpStatus.OK); - assertNull(this.service.sessionId); + // Info or greeting requests must be first so "/a" is cached as a known prefix + handleRequest("GET", "/a/info", HttpStatus.OK); + handleRequest("GET", "/a/server/session/xhr", HttpStatus.OK); + + assertEquals("session", this.service.sessionId); + assertEquals(TransportType.XHR, this.service.transportType); assertSame(this.handler, this.service.handler); + } - handleRequest("/echo/server1/session2/xhr", HttpStatus.OK); - assertEquals("session2", this.service.sessionId); + @Test + public void getSockJsPathForTransportRequestWithConfiguredPrefix() throws Exception { + + this.service.setValidSockJsPrefixes("/a"); + handleRequest("GET", "/a/server/session/xhr", HttpStatus.OK); + + assertEquals("session", this.service.sessionId); assertEquals(TransportType.XHR, this.service.transportType); assertSame(this.handler, this.service.handler); + } + + @Test + public void validateRequest() throws Exception { - handleRequest("/echo/other", HttpStatus.NOT_FOUND); - handleRequest("/echo//", HttpStatus.NOT_FOUND); - handleRequest("/echo///", HttpStatus.NOT_FOUND); + this.service.setValidSockJsPrefixes("/echo"); + + this.service.setWebSocketsEnabled(false); + handleRequest("GET", "/echo/server/session/websocket", HttpStatus.NOT_FOUND); + + this.service.setWebSocketsEnabled(true); + handleRequest("GET", "/echo/server/session/websocket", HttpStatus.OK); + + handleRequest("GET", "/echo//", HttpStatus.NOT_FOUND); + handleRequest("GET", "/echo///", HttpStatus.NOT_FOUND); + handleRequest("GET", "/echo/other", HttpStatus.NOT_FOUND); + handleRequest("GET", "/echo//service/websocket", HttpStatus.NOT_FOUND); + handleRequest("GET", "/echo/server//websocket", HttpStatus.NOT_FOUND); + handleRequest("GET", "/echo/server/session/", HttpStatus.NOT_FOUND); + handleRequest("GET", "/echo/s.erver/session/websocket", HttpStatus.NOT_FOUND); + handleRequest("GET", "/echo/server/s.ession/websocket", HttpStatus.NOT_FOUND); } + @Test + public void handleInfoGet() throws Exception { + + handleRequest("GET", "/a/info", HttpStatus.OK); + + assertEquals("application/json;charset=UTF-8", this.servletResponse.getContentType()); + assertEquals("*", this.servletResponse.getHeader("Access-Control-Allow-Origin")); + assertEquals("true", this.servletResponse.getHeader("Access-Control-Allow-Credentials")); + assertEquals("no-store, no-cache, must-revalidate, max-age=0", this.servletResponse.getHeader("Cache-Control")); + + String body = this.servletResponse.getContentAsString(); + assertEquals("{\"entropy\"", body.substring(0, body.indexOf(':'))); + assertEquals(",\"origins\":[\"*:*\"],\"cookie_needed\":true,\"websocket\":true}", + body.substring(body.indexOf(','))); + + this.service.setJsessionIdCookieRequired(false); + this.service.setWebSocketsEnabled(false); + handleRequest("GET", "/a/info", HttpStatus.OK); + + body = this.servletResponse.getContentAsString(); + assertEquals(",\"origins\":[\"*:*\"],\"cookie_needed\":false,\"websocket\":false}", + body.substring(body.indexOf(','))); + } @Test - public void getSockJsPathGreetingRequest() throws Exception { - handleRequest("/echo", HttpStatus.OK); - assertEquals("Welcome to SockJS!\n", this.servletResponse.getContentAsString()); + public void handleInfoOptions() throws Exception { + + this.servletRequest.addHeader("Access-Control-Request-Headers", "Last-Modified"); + + handleRequest("OPTIONS", "/a/info", HttpStatus.NO_CONTENT); + + assertEquals("*", this.servletResponse.getHeader("Access-Control-Allow-Origin")); + assertEquals("true", this.servletResponse.getHeader("Access-Control-Allow-Credentials")); + assertEquals("Last-Modified", this.servletResponse.getHeader("Access-Control-Allow-Headers")); + assertEquals("OPTIONS, GET", this.servletResponse.getHeader("Access-Control-Allow-Methods")); + assertEquals("31536000", this.servletResponse.getHeader("Access-Control-Max-Age")); } @Test - public void getSockJsPathInfoRequest() throws Exception { - handleRequest("/echo/info", HttpStatus.OK); - assertTrue(this.servletResponse.getContentAsString().startsWith("{\"entropy\":")); + public void handleIframeRequest() throws Exception { + + this.service.setValidSockJsPrefixes("/a"); + handleRequest("GET", "/a/iframe.html", HttpStatus.OK); + + assertEquals("text/html;charset=UTF-8", this.servletResponse.getContentType()); + assertTrue(this.servletResponse.getContentAsString().startsWith("\n")); + assertEquals(496, this.servletResponse.getContentLength()); + assertEquals("public, max-age=31536000", this.response.getHeaders().getCacheControl()); + assertEquals("\"0da1ed070012f304e47b83c81c48ad620\"", response.getHeaders().getETag()); } @Test - public void getSockJsPathWithConfiguredPrefix() throws Exception { - this.service.setValidSockJsPrefixes("/echo"); - handleRequest("/echo/s1/s2/xhr", HttpStatus.OK); + public void handleIframeRequestNotModified() throws Exception { + + this.servletRequest.addHeader("If-None-Match", "\"0da1ed070012f304e47b83c81c48ad620\""); + + this.service.setValidSockJsPrefixes("/a"); + handleRequest("GET", "/a/iframe.html", HttpStatus.NOT_MODIFIED); } @Test - public void getInfoOptions() throws Exception { - setRequest("OPTIONS", "/echo/info"); - this.service.handleRequest(this.request, this.response, this.handler); + public void handleRawWebSocketRequest() throws Exception { - assertEquals(204, servletResponse.getStatus()); + handleRequest("GET", "/a", HttpStatus.OK); + assertEquals("Welcome to SockJS!\n", this.servletResponse.getContentAsString()); + + handleRequest("GET", "/a/websocket", HttpStatus.OK); + assertNull("Raw WebSocket should not open a SockJS session", this.service.sessionId); + assertSame(this.handler, this.service.handler); } - private void handleRequest(String uri, HttpStatus httpStatus) throws IOException { + private void handleRequest(String httpMethod, String uri, HttpStatus httpStatus) throws IOException { resetResponse(); - setRequest("GET", uri); + setRequest(httpMethod, uri); this.service.handleRequest(this.request, this.response, this.handler); assertEquals(httpStatus.value(), this.servletResponse.getStatus()); diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsSessionTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsSessionTests.java new file mode 100644 index 0000000000..891cde37ee --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/AbstractSockJsSessionTests.java @@ -0,0 +1,269 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.sockjs; + +import java.io.IOException; +import java.sql.Date; +import java.util.Collections; +import java.util.concurrent.ScheduledFuture; + +import org.junit.Test; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + + +/** + * Test fixture for {@link AbstractSockJsSession}. + * + * @author Rossen Stoyanchev + */ +public class AbstractSockJsSessionTests extends BaseAbstractSockJsSessionTests { + + + @Override + protected TestSockJsSession initSockJsSession() { + return new TestSockJsSession("1", this.sockJsConfig, this.webSocketHandler); + } + + @Test + public void getTimeSinceLastActive() throws Exception { + + Thread.sleep(1); + + long time1 = this.session.getTimeSinceLastActive(); + assertTrue(time1 > 0); + + Thread.sleep(1); + + long time2 = this.session.getTimeSinceLastActive(); + assertTrue(time2 > time1); + + this.session.delegateConnectionEstablished(); + + Thread.sleep(1); + + this.session.setActive(false); + assertTrue(this.session.getTimeSinceLastActive() > 0); + + this.session.setActive(true); + assertEquals(0, this.session.getTimeSinceLastActive()); + } + + @Test + public void delegateConnectionEstablished() throws Exception { + assertNew(); + this.session.delegateConnectionEstablished(); + assertOpen(); + verify(this.webSocketHandler).afterConnectionEstablished(this.session); + } + + @Test + public void delegateError() throws Exception { + Exception ex = new Exception(); + this.session.delegateError(ex); + verify(this.webSocketHandler).handleTransportError(this.session, ex); + } + + @Test + public void delegateMessages() throws Exception { + String msg1 = "message 1"; + String msg2 = "message 2"; + this.session.delegateMessages(new String[] { msg1, msg2 }); + + verify(this.webSocketHandler).handleMessage(this.session, new TextMessage(msg1)); + verify(this.webSocketHandler).handleMessage(this.session, new TextMessage(msg2)); + verifyNoMoreInteractions(this.webSocketHandler); + } + + @Test + public void delegateConnectionClosed() throws Exception { + this.session.delegateConnectionEstablished(); + this.session.delegateConnectionClosed(CloseStatus.GOING_AWAY); + + assertClosed(); + assertEquals(1, this.session.getNumberOfLastActiveTimeUpdates()); + assertTrue(this.session.didCancelHeartbeat()); + verify(this.webSocketHandler).afterConnectionClosed(this.session, CloseStatus.GOING_AWAY); + } + + @Test + public void closeWhenNotOpen() throws Exception { + + assertNew(); + + this.session.close(); + assertNull("Close not ignored for a new session", this.session.getStatus()); + + this.session.delegateConnectionEstablished(); + assertOpen(); + + this.session.close(); + assertClosed(); + assertEquals(3000, this.session.getStatus().getCode()); + + this.session.close(CloseStatus.SERVER_ERROR); + assertEquals("Close should be ignored if already closed", 3000, this.session.getStatus().getCode()); + } + + @Test + public void closeWhenNotActive() throws Exception { + + this.session.delegateConnectionEstablished(); + assertOpen(); + + this.session.setActive(false); + this.session.close(); + + assertEquals(Collections.emptyList(), this.session.getSockJsFramesWritten()); + } + + @Test + public void close() throws Exception { + + this.session.delegateConnectionEstablished(); + assertOpen(); + + this.session.setActive(true); + this.session.close(); + + assertEquals(1, this.session.getSockJsFramesWritten().size()); + assertEquals(SockJsFrame.closeFrameGoAway(), this.session.getSockJsFramesWritten().get(0)); + + assertEquals(1, this.session.getNumberOfLastActiveTimeUpdates()); + assertTrue(this.session.didCancelHeartbeat()); + + assertEquals(new CloseStatus(3000, "Go away!"), this.session.getStatus()); + assertClosed(); + verify(this.webSocketHandler).afterConnectionClosed(this.session, new CloseStatus(3000, "Go away!")); + } + + @Test + public void closeWithWriteFrameExceptions() throws Exception { + + this.session.setExceptionOnWriteFrame(new IOException()); + + this.session.delegateConnectionEstablished(); + this.session.setActive(true); + this.session.close(); + + assertEquals(new CloseStatus(3000, "Go away!"), this.session.getStatus()); + assertClosed(); + } + + @Test + public void closeWithWebSocketHandlerExceptions() throws Exception { + + doThrow(new Exception()).when(this.webSocketHandler).afterConnectionClosed(this.session, CloseStatus.NORMAL); + + this.session.delegateConnectionEstablished(); + this.session.setActive(true); + this.session.close(CloseStatus.NORMAL); + + assertEquals(CloseStatus.NORMAL, this.session.getStatus()); + assertClosed(); + } + + @Test + public void writeFrame() throws Exception { + this.session.writeFrame(SockJsFrame.openFrame()); + + assertEquals(1, this.session.getSockJsFramesWritten().size()); + assertEquals(SockJsFrame.openFrame(), this.session.getSockJsFramesWritten().get(0)); + } + + @Test + public void writeFrameIoException() throws Exception { + this.session.setExceptionOnWriteFrame(new IOException()); + this.session.delegateConnectionEstablished(); + try { + this.session.writeFrame(SockJsFrame.openFrame()); + fail("expected exception"); + } + catch (IOException ex) { + assertEquals(CloseStatus.SERVER_ERROR, this.session.getStatus()); + verify(this.webSocketHandler).afterConnectionClosed(this.session, CloseStatus.SERVER_ERROR); + } + } + + @Test + public void writeFrameThrowable() throws Exception { + this.session.setExceptionOnWriteFrame(new NullPointerException()); + this.session.delegateConnectionEstablished(); + try { + this.session.writeFrame(SockJsFrame.openFrame()); + fail("expected exception"); + } + catch (SockJsRuntimeException ex) { + assertEquals(CloseStatus.SERVER_ERROR, this.session.getStatus()); + verify(this.webSocketHandler).afterConnectionClosed(this.session, CloseStatus.SERVER_ERROR); + } + } + + @Test + public void sendHeartbeatWhenNotActive() throws Exception { + this.session.setActive(false); + this.session.sendHeartbeat(); + + assertEquals(Collections.emptyList(), this.session.getSockJsFramesWritten()); + } + + @Test + public void sendHeartbeat() throws Exception { + this.session.setActive(true); + this.session.sendHeartbeat(); + + assertEquals(1, this.session.getSockJsFramesWritten().size()); + assertEquals(SockJsFrame.heartbeatFrame(), this.session.getSockJsFramesWritten().get(0)); + + verify(this.taskScheduler).schedule(any(Runnable.class), any(Date.class)); + verifyNoMoreInteractions(this.taskScheduler); + } + + @Test + public void scheduleHeartbeatNotActive() throws Exception { + this.session.setActive(false); + this.session.scheduleHeartbeat(); + + verifyNoMoreInteractions(this.taskScheduler); + } + + @Test + public void scheduleAndCancelHeartbeat() throws Exception { + + ScheduledFuture task = mock(ScheduledFuture.class); + doReturn(task).when(this.taskScheduler).schedule(any(Runnable.class), any(Date.class)); + + this.session.setActive(true); + this.session.scheduleHeartbeat(); + + verify(this.taskScheduler).schedule(any(Runnable.class), any(Date.class)); + verifyNoMoreInteractions(this.taskScheduler); + + doReturn(false).when(task).isDone(); + + this.session.cancelHeartbeat(); + + verify(task).isDone(); + verify(task).cancel(false); + verifyNoMoreInteractions(task); + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/BaseAbstractSockJsSessionTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/BaseAbstractSockJsSessionTests.java new file mode 100644 index 0000000000..9d4f50c60d --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/BaseAbstractSockJsSessionTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.sockjs; + +import org.junit.Before; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.web.socket.WebSocketHandler; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + + +/** + * Base class for {@link AbstractSockJsSession} classes. + * + * @author Rossen Stoyanchev + */ +public abstract class BaseAbstractSockJsSessionTests { + + protected WebSocketHandler webSocketHandler; + + protected StubSockJsConfig sockJsConfig; + + protected TaskScheduler taskScheduler; + + protected S session; + + + @Before + public void setUp() { + this.webSocketHandler = mock(WebSocketHandler.class); + this.taskScheduler = mock(TaskScheduler.class); + + this.sockJsConfig = new StubSockJsConfig(); + this.sockJsConfig.setTaskScheduler(this.taskScheduler); + + this.session = initSockJsSession(); + } + + protected abstract S initSockJsSession(); + + protected void assertNew() { + assertState(true, false, false); + } + + protected void assertOpen() { + assertState(false, true, false); + } + + protected void assertClosed() { + assertState(false, false, true); + } + + private void assertState(boolean isNew, boolean isOpen, boolean isClosed) { + assertEquals(isNew, this.session.isNew()); + assertEquals(isOpen, this.session.isOpen()); + assertEquals(isClosed, this.session.isClosed()); + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/TestSockJsSession.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/TestSockJsSession.java new file mode 100644 index 0000000000..1bb00fb037 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/TestSockJsSession.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.sockjs; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.WebSocketHandler; + + +/** + * @author Rossen Stoyanchev + */ +public class TestSockJsSession extends AbstractSockJsSession { + + private boolean active; + + private final List sockJsFramesWritten = new ArrayList<>(); + + private CloseStatus status; + + private Exception exceptionOnWriteFrame; + + private int numberOfLastActiveTimeUpdates; + + private boolean cancelledHeartbeat; + + + public TestSockJsSession(String sessionId, SockJsConfiguration config, WebSocketHandler handler) { + super(sessionId, config, handler); + } + + public CloseStatus getStatus() { + return this.status; + } + + @Override + public boolean isActive() { + return this.active; + } + + public void setActive(boolean active) { + this.active = active; + } + + public List getSockJsFramesWritten() { + return this.sockJsFramesWritten; + } + + public void setExceptionOnWriteFrame(Exception exceptionOnWriteFrame) { + this.exceptionOnWriteFrame = exceptionOnWriteFrame; + } + + public int getNumberOfLastActiveTimeUpdates() { + return this.numberOfLastActiveTimeUpdates; + } + + public boolean didCancelHeartbeat() { + return this.cancelledHeartbeat; + } + + @Override + protected void updateLastActiveTime() { + this.numberOfLastActiveTimeUpdates++; + super.updateLastActiveTime(); + } + + @Override + protected void cancelHeartbeat() { + this.cancelledHeartbeat = true; + super.cancelHeartbeat(); + } + + @Override + protected void sendMessageInternal(String message) throws IOException { + } + + @Override + protected void writeFrameInternal(SockJsFrame frame) throws Exception { + this.sockJsFramesWritten.add(frame); + if (this.exceptionOnWriteFrame != null) { + throw exceptionOnWriteFrame; + } + } + + @Override + protected void disconnect(CloseStatus status) throws IOException { + this.status = status; + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/DefaultSockJsServiceTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/DefaultSockJsServiceTests.java index ccf42cea43..4c91e582e1 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/DefaultSockJsServiceTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/support/DefaultSockJsServiceTests.java @@ -16,17 +16,30 @@ package org.springframework.web.socket.sockjs.support; +import java.util.Collections; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.junit.Before; import org.junit.Test; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.scheduling.TaskScheduler; import org.springframework.web.socket.AbstractHttpRequestTests; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.sockjs.AbstractSockJsSession; +import org.springframework.web.socket.sockjs.SockJsSessionFactory; +import org.springframework.web.socket.sockjs.StubSockJsConfig; +import org.springframework.web.socket.sockjs.TestSockJsSession; +import org.springframework.web.socket.sockjs.TransportErrorException; import org.springframework.web.socket.sockjs.TransportHandler; import org.springframework.web.socket.sockjs.TransportType; import static org.junit.Assert.*; - +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; /** * Test fixture for {@link DefaultSockJsService}. @@ -35,20 +48,17 @@ import static org.junit.Assert.*; */ public class DefaultSockJsServiceTests extends AbstractHttpRequestTests { - private DefaultSockJsService service; - @Override @Before public void setUp() { super.setUp(); - this.service = new DefaultSockJsService(new ThreadPoolTaskScheduler()); - this.service.setValidSockJsPrefixes("/echo"); } @Test public void defaultTransportHandlers() { + DefaultSockJsService service = new DefaultSockJsService(mock(TaskScheduler.class)); Map handlers = service.getTransportHandlers(); assertEquals(8, handlers.size()); @@ -62,4 +72,128 @@ public class DefaultSockJsServiceTests extends AbstractHttpRequestTests { assertNotNull(handlers.get(TransportType.EVENT_SOURCE)); } + @Test + public void handleTransportRequestXhr() throws Exception { + + setRequest("POST", "/a/server/session/xhr"); + + TaskScheduler taskScheduler = mock(TaskScheduler.class); + StubXhrTransportHandler xhrHandler = new StubXhrTransportHandler(); + Set transportHandlers = Collections.singleton(xhrHandler); + WebSocketHandler webSocketHandler = mock(WebSocketHandler.class); + + DefaultSockJsService service = new DefaultSockJsService(taskScheduler, transportHandlers); + service.handleTransportRequest(this.request, this.response, "123", TransportType.XHR, webSocketHandler); + + assertEquals(200, this.servletResponse.getStatus()); + assertNotNull(xhrHandler.session); + assertSame(webSocketHandler, xhrHandler.webSocketHandler); + + verify(taskScheduler).scheduleAtFixedRate(any(Runnable.class), eq(service.getDisconnectDelay())); + + assertEquals("no-store, no-cache, must-revalidate, max-age=0", this.response.getHeaders().getCacheControl()); + assertEquals("JSESSIONID=dummy;path=/", this.response.getHeaders().getFirst("Set-Cookie")); + assertEquals("*", this.response.getHeaders().getFirst("Access-Control-Allow-Origin")); + assertEquals("true", this.response.getHeaders().getFirst("Access-Control-Allow-Credentials")); + } + + @Test + public void handleTransportRequestXhrOptions() throws Exception { + + setRequest("OPTIONS", "/a/server/session/xhr"); + + TaskScheduler taskScheduler = mock(TaskScheduler.class); + StubXhrTransportHandler xhrHandler = new StubXhrTransportHandler(); + Set transportHandlers = Collections.singleton(xhrHandler); + + DefaultSockJsService service = new DefaultSockJsService(taskScheduler, transportHandlers); + service.handleTransportRequest(this.request, this.response, "123", TransportType.XHR, null); + + assertEquals(204, this.servletResponse.getStatus()); + assertEquals("*", this.response.getHeaders().getFirst("Access-Control-Allow-Origin")); + assertEquals("true", this.response.getHeaders().getFirst("Access-Control-Allow-Credentials")); + assertEquals("OPTIONS, POST", this.response.getHeaders().getFirst("Access-Control-Allow-Methods")); + } + + @Test + public void handleTransportRequestNoSuitableHandler() throws Exception { + + setRequest("POST", "/a/server/session/xhr"); + + Set transportHandlers = new HashSet<>(); + DefaultSockJsService service = new DefaultSockJsService(mock(TaskScheduler.class), transportHandlers); + service.handleTransportRequest(this.request, this.response, "123", TransportType.XHR, null); + + assertEquals(404, this.servletResponse.getStatus()); + } + + @Test + public void handleTransportRequestXhrSend() throws Exception { + + this.servletRequest.setMethod("POST"); + + Set transportHandlers = new HashSet<>(); + transportHandlers.add(new StubXhrTransportHandler()); + transportHandlers.add(new StubXhrSendTransportHandler()); + WebSocketHandler webSocketHandler = mock(WebSocketHandler.class); + DefaultSockJsService service = new DefaultSockJsService(mock(TaskScheduler.class), transportHandlers); + + service.handleTransportRequest(this.request, this.response, "123", TransportType.XHR_SEND, webSocketHandler); + + assertEquals(404, this.servletResponse.getStatus()); // dropped (no session) + + resetResponse(); + service.handleTransportRequest(this.request, this.response, "123", TransportType.XHR, webSocketHandler); + + assertEquals(200, this.servletResponse.getStatus()); + + resetResponse(); + service.handleTransportRequest(this.request, this.response, "123", TransportType.XHR_SEND, webSocketHandler); + + assertEquals(200, this.servletResponse.getStatus()); + } + + + private static class StubXhrTransportHandler implements TransportHandler, SockJsSessionFactory { + + WebSocketHandler webSocketHandler; + + AbstractSockJsSession session; + + @Override + public TransportType getTransportType() { + return TransportType.XHR; + } + + @Override + public void handleRequest(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler handler, AbstractSockJsSession session) throws TransportErrorException { + + this.webSocketHandler = handler; + this.session = session; + } + + @Override + public AbstractSockJsSession createSession(String sessionId, WebSocketHandler webSocketHandler) { + return new TestSockJsSession(sessionId, new StubSockJsConfig(), webSocketHandler); + } + } + + private static class StubXhrSendTransportHandler implements TransportHandler { + + @Override + public TransportType getTransportType() { + return TransportType.XHR_SEND; + } + + @Override + public void handleRequest(ServerHttpRequest request, ServerHttpResponse response, + WebSocketHandler handler, AbstractSockJsSession session) throws TransportErrorException { + + if (session == null) { + response.setStatusCode(HttpStatus.NOT_FOUND); + } + } + } + } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSockJsSessionTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSockJsSessionTests.java new file mode 100644 index 0000000000..d9ccacedd1 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/AbstractHttpSockJsSessionTests.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.sockjs.transport; + +import java.io.IOException; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.server.AsyncServletServerHttpRequest; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.mock.web.test.MockHttpServletRequest; +import org.springframework.mock.web.test.MockHttpServletResponse; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.sockjs.BaseAbstractSockJsSessionTests; +import org.springframework.web.socket.sockjs.SockJsConfiguration; +import org.springframework.web.socket.sockjs.SockJsFrame; +import org.springframework.web.socket.sockjs.SockJsFrame.DefaultFrameFormat; +import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat; +import org.springframework.web.socket.sockjs.transport.AbstractHttpSockJsSessionTests.TestAbstractHttpSockJsSession; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Test fixture for {@link AbstractHttpSockJsSession}. + * + * @author Rossen Stoyanchev + */ +public class AbstractHttpSockJsSessionTests extends BaseAbstractSockJsSessionTests { + + protected ServerHttpRequest request; + + protected ServerHttpResponse response; + + protected MockHttpServletRequest servletRequest; + + protected MockHttpServletResponse servletResponse; + + private FrameFormat frameFormat; + + + @Before + public void setup() { + + super.setUp(); + + this.frameFormat = new DefaultFrameFormat("%s"); + + this.servletResponse = new MockHttpServletResponse(); + this.response = new ServletServerHttpResponse(this.servletResponse); + + this.servletRequest = new MockHttpServletRequest(); + this.servletRequest.setAsyncSupported(true); + this.request = new AsyncServletServerHttpRequest(this.servletRequest, this.servletResponse); + } + + @Override + protected TestAbstractHttpSockJsSession initSockJsSession() { + return new TestAbstractHttpSockJsSession(this.sockJsConfig, this.webSocketHandler); + } + + @Test + public void setInitialRequest() throws Exception { + + this.session.setInitialRequest(this.request, this.response, this.frameFormat); + + assertTrue(this.session.hasRequest()); + assertTrue(this.session.hasResponse()); + + assertEquals("o", this.servletResponse.getContentAsString()); + assertFalse(this.servletRequest.isAsyncStarted()); + + verify(this.webSocketHandler).afterConnectionEstablished(this.session); + } + + @Test + public void setLongPollingRequest() throws Exception { + + this.session.getMessageCache().add("x"); + this.session.setLongPollingRequest(this.request, this.response, this.frameFormat); + + assertTrue(this.session.hasRequest()); + assertTrue(this.session.hasResponse()); + assertTrue(this.servletRequest.isAsyncStarted()); + + assertTrue(this.session.wasHeartbeatScheduled()); + assertTrue(this.session.wasCacheFlushed()); + + verifyNoMoreInteractions(this.webSocketHandler); + } + + @Test + public void setLongPollingRequestWhenClosed() throws Exception { + + this.session.delegateConnectionClosed(CloseStatus.NORMAL); + assertClosed(); + + this.session.setLongPollingRequest(this.request, this.response, this.frameFormat); + + assertEquals("c[3000,\"Go away!\"]", this.servletResponse.getContentAsString()); + assertFalse(this.servletRequest.isAsyncStarted()); + } + + + static class TestAbstractHttpSockJsSession extends AbstractHttpSockJsSession { + + private IOException exceptionOnWriteFrame; + + private boolean cacheFlushed; + + private boolean heartbeatScheduled; + + + public TestAbstractHttpSockJsSession(SockJsConfiguration config, WebSocketHandler handler) { + super("1", config, handler); + } + + public boolean wasCacheFlushed() { + return this.cacheFlushed; + } + + public boolean wasHeartbeatScheduled() { + return this.heartbeatScheduled; + } + + public boolean hasRequest() { + return getRequest() != null; + } + + public boolean hasResponse() { + return getResponse() != null; + } + + public void setExceptionOnWriteFrame(IOException exceptionOnWriteFrame) { + this.exceptionOnWriteFrame = exceptionOnWriteFrame; + } + + @Override + protected void flushCache() throws IOException { + this.cacheFlushed = true; + } + + @Override + protected void scheduleHeartbeat() { + this.heartbeatScheduled = true; + } + + @Override + protected synchronized void writeFrameInternal(SockJsFrame frame) throws IOException { + if (this.exceptionOnWriteFrame != null) { + throw this.exceptionOnWriteFrame; + } + else { + super.writeFrameInternal(frame); + } + } + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/HttpReceivingTransportHandlerTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/HttpReceivingTransportHandlerTests.java new file mode 100644 index 0000000000..244c2edb6c --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/HttpReceivingTransportHandlerTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.sockjs.transport; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.web.socket.AbstractHttpRequestTests; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.sockjs.AbstractSockJsSession; +import org.springframework.web.socket.sockjs.StubSockJsConfig; +import org.springframework.web.socket.sockjs.TestSockJsSession; +import org.springframework.web.socket.sockjs.TransportErrorException; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Test fixture for {@link AbstractHttpReceivingTransportHandler} and sub-classes + * {@link XhrTransportHandler} and {@link JsonpTransportHandler}. + * + * @author Rossen Stoyanchev + */ +public class HttpReceivingTransportHandlerTests extends AbstractHttpRequestTests { + + + @Override + @Before + public void setUp() { + super.setUp(); + } + + @Test + public void readMessagesXhr() throws Exception { + this.servletRequest.setContent("[\"x\"]".getBytes("UTF-8")); + handleRequest(new XhrTransportHandler()); + + assertEquals(204, this.servletResponse.getStatus()); + } + + @Test + public void readMessagesJsonp() throws Exception { + this.servletRequest.setContent("[\"x\"]".getBytes("UTF-8")); + handleRequest(new JsonpTransportHandler()); + + assertEquals(200, this.servletResponse.getStatus()); + assertEquals("ok", this.servletResponse.getContentAsString()); + } + + @Test + public void readMessagesJsonpFormEncoded() throws Exception { + this.servletRequest.setContent("d=[\"x\"]".getBytes("UTF-8")); + this.servletRequest.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE); + handleRequest(new JsonpTransportHandler()); + + assertEquals(200, this.servletResponse.getStatus()); + assertEquals("ok", this.servletResponse.getContentAsString()); + } + + @Test + public void readMessagesBadContent() throws Exception { + this.servletRequest.setContent("".getBytes("UTF-8")); + handleRequestAndExpectFailure(); + + this.servletRequest.setContent("[\"x]".getBytes("UTF-8")); + handleRequestAndExpectFailure(); + } + + @Test + public void readMessagesNoSession() throws Exception { + WebSocketHandler webSocketHandler = mock(WebSocketHandler.class); + new XhrTransportHandler().handleRequest(this.request, this.response, webSocketHandler, null); + + assertEquals(404, this.servletResponse.getStatus()); + } + + @Test + public void delegateMessageException() throws Exception { + + this.servletRequest.setContent("[\"x\"]".getBytes("UTF-8")); + + WebSocketHandler webSocketHandler = mock(WebSocketHandler.class); + TestSockJsSession session = new TestSockJsSession("1", new StubSockJsConfig(), webSocketHandler); + session.delegateConnectionEstablished(); + + doThrow(new Exception()).when(webSocketHandler).handleMessage(session, new TextMessage("x")); + + try { + new XhrTransportHandler().handleRequest(this.request, this.response, webSocketHandler, session); + fail("Expected exception"); + } + catch (TransportErrorException ex) { + assertEquals(CloseStatus.SERVER_ERROR, session.getStatus()); + } + } + + + private void handleRequest(AbstractHttpReceivingTransportHandler transportHandler) + throws Exception { + + WebSocketHandler webSocketHandler = mock(WebSocketHandler.class); + AbstractSockJsSession session = new TestSockJsSession("1", new StubSockJsConfig(), webSocketHandler); + + transportHandler.handleRequest(this.request, this.response, webSocketHandler, session); + + assertEquals("text/plain;charset=UTF-8", this.response.getHeaders().getContentType().toString()); + verify(webSocketHandler).handleMessage(session, new TextMessage("x")); + } + + private void handleRequestAndExpectFailure() throws Exception { + + resetResponse(); + + WebSocketHandler webSocketHandler = mock(WebSocketHandler.class); + AbstractSockJsSession session = new TestSockJsSession("1", new StubSockJsConfig(), webSocketHandler); + + new XhrTransportHandler().handleRequest(this.request, this.response, webSocketHandler, session); + + assertEquals(500, this.servletResponse.getStatus()); + verifyNoMoreInteractions(webSocketHandler); + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/HttpSendingTransportHandlerTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/HttpSendingTransportHandlerTests.java new file mode 100644 index 0000000000..68383088a3 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/HttpSendingTransportHandlerTests.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.sockjs.transport; + +import java.sql.Date; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.web.socket.AbstractHttpRequestTests; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.sockjs.AbstractSockJsSession; +import org.springframework.web.socket.sockjs.SockJsFrame; +import org.springframework.web.socket.sockjs.SockJsFrame.FrameFormat; +import org.springframework.web.socket.sockjs.StubSockJsConfig; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + +/** + * Test fixture for {@link AbstractHttpSendingTransportHandler} and sub-classes. + * + * @author Rossen Stoyanchev + */ +public class HttpSendingTransportHandlerTests extends AbstractHttpRequestTests { + + private WebSocketHandler webSocketHandler; + + private StubSockJsConfig sockJsConfig; + + private TaskScheduler taskScheduler; + + + @Override + @Before + public void setUp() { + super.setUp(); + + this.webSocketHandler = mock(WebSocketHandler.class); + this.taskScheduler = mock(TaskScheduler.class); + + this.sockJsConfig = new StubSockJsConfig(); + this.sockJsConfig.setTaskScheduler(this.taskScheduler); + } + + @Test + public void handleRequestXhr() throws Exception { + + XhrPollingTransportHandler transportHandler = new XhrPollingTransportHandler(); + transportHandler.setSockJsConfiguration(sockJsConfig); + + AbstractSockJsSession session = transportHandler.createSession("1", webSocketHandler); + transportHandler.handleRequest(request, response, webSocketHandler, session); + + assertEquals("application/javascript;charset=UTF-8", this.response.getHeaders().getContentType().toString()); + assertEquals("o\n", this.servletResponse.getContentAsString()); + assertFalse("Polling request should complete after open frame", this.servletRequest.isAsyncStarted()); + verify(webSocketHandler).afterConnectionEstablished(session); + + resetResponse(); + transportHandler.handleRequest(request, response, webSocketHandler, session); + + assertTrue("Polling request should remain open", this.servletRequest.isAsyncStarted()); + verify(this.taskScheduler).schedule(any(Runnable.class), any(Date.class)); + + resetRequestAndResponse(); + transportHandler.handleRequest(request, response, webSocketHandler, session); + + assertFalse("Request should have been rejected", this.servletRequest.isAsyncStarted()); + assertEquals("c[2010,\"Another connection still open\"]\n", this.servletResponse.getContentAsString()); + } + + @Test + public void jsonpTransport() throws Exception { + + JsonpPollingTransportHandler transportHandler = new JsonpPollingTransportHandler(); + transportHandler.setSockJsConfiguration(sockJsConfig); + PollingSockJsSession session = transportHandler.createSession("1", webSocketHandler); + + transportHandler.handleRequest(request, response, webSocketHandler, session); + + assertEquals(500, this.servletResponse.getStatus()); + assertEquals("\"callback\" parameter required", this.servletResponse.getContentAsString()); + + resetRequestAndResponse(); + this.servletRequest.addParameter("c", "callback"); + transportHandler.handleRequest(request, response, webSocketHandler, session); + + assertEquals("application/javascript;charset=UTF-8", this.response.getHeaders().getContentType().toString()); + assertFalse("Polling request should complete after open frame", this.servletRequest.isAsyncStarted()); + verify(webSocketHandler).afterConnectionEstablished(session); + } + + @Test + public void handleRequestXhrStreaming() throws Exception { + + XhrStreamingTransportHandler transportHandler = new XhrStreamingTransportHandler(); + transportHandler.setSockJsConfiguration(sockJsConfig); + AbstractSockJsSession session = transportHandler.createSession("1", webSocketHandler); + + transportHandler.handleRequest(request, response, webSocketHandler, session); + + assertEquals("application/javascript;charset=UTF-8", this.response.getHeaders().getContentType().toString()); + assertTrue("Streaming request not started", this.servletRequest.isAsyncStarted()); + verify(webSocketHandler).afterConnectionEstablished(session); + } + + @Test + public void htmlFileTransport() throws Exception { + + HtmlFileTransportHandler transportHandler = new HtmlFileTransportHandler(); + transportHandler.setSockJsConfiguration(sockJsConfig); + StreamingSockJsSession session = transportHandler.createSession("1", webSocketHandler); + + transportHandler.handleRequest(request, response, webSocketHandler, session); + + assertEquals(500, this.servletResponse.getStatus()); + assertEquals("\"callback\" parameter required", this.servletResponse.getContentAsString()); + + resetRequestAndResponse(); + this.servletRequest.addParameter("c", "callback"); + transportHandler.handleRequest(request, response, webSocketHandler, session); + + assertEquals("text/html;charset=UTF-8", this.response.getHeaders().getContentType().toString()); + assertTrue("Streaming request not started", this.servletRequest.isAsyncStarted()); + verify(webSocketHandler).afterConnectionEstablished(session); + } + + @Test + public void eventSourceTransport() throws Exception { + + EventSourceTransportHandler transportHandler = new EventSourceTransportHandler(); + transportHandler.setSockJsConfiguration(sockJsConfig); + StreamingSockJsSession session = transportHandler.createSession("1", webSocketHandler); + + transportHandler.handleRequest(request, response, webSocketHandler, session); + + assertEquals("text/event-stream;charset=UTF-8", this.response.getHeaders().getContentType().toString()); + assertTrue("Streaming request not started", this.servletRequest.isAsyncStarted()); + verify(webSocketHandler).afterConnectionEstablished(session); + } + + @Test + public void frameFormats() throws Exception { + + this.servletRequest.addParameter("c", "callback"); + + SockJsFrame frame = SockJsFrame.openFrame(); + + FrameFormat format = new XhrPollingTransportHandler().getFrameFormat(request); + SockJsFrame formatted = format.format(frame); + assertEquals(frame.getContent() + "\n", formatted.getContent()); + + format = new XhrStreamingTransportHandler().getFrameFormat(request); + formatted = format.format(frame); + assertEquals(frame.getContent() + "\n", formatted.getContent()); + + format = new HtmlFileTransportHandler().getFrameFormat(request); + formatted = format.format(frame); + assertEquals("\r\n", formatted.getContent()); + + format = new EventSourceTransportHandler().getFrameFormat(request); + formatted = format.format(frame); + assertEquals("data: " + frame.getContent() + "\r\n\r\n", formatted.getContent()); + + format = new JsonpPollingTransportHandler().getFrameFormat(request); + formatted = format.format(frame); + assertEquals("callback(\"" + frame.getContent() + "\");\r\n", formatted.getContent()); + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSessionTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSessionTests.java new file mode 100644 index 0000000000..a145541a8a --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/transport/WebSocketServerSockJsSessionTests.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.sockjs.transport; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.sockjs.BaseAbstractSockJsSessionTests; +import org.springframework.web.socket.sockjs.SockJsConfiguration; +import org.springframework.web.socket.sockjs.transport.WebSocketServerSockJsSessionTests.TestWebSocketServerSockJsSession; +import org.springframework.web.socket.support.TestWebSocketSession; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; + + +/** + * Test fixture for {@link WebSocketServerSockJsSession}. + * + * @author Rossen Stoyanchev + */ +public class WebSocketServerSockJsSessionTests extends BaseAbstractSockJsSessionTests { + + private TestWebSocketSession webSocketSession; + + + @Before + public void setup() { + super.setUp(); + this.webSocketSession = new TestWebSocketSession(); + this.webSocketSession.setOpen(true); + } + + @Override + protected TestWebSocketServerSockJsSession initSockJsSession() { + return new TestWebSocketServerSockJsSession(this.sockJsConfig, this.webSocketHandler); + } + + @Test + public void isActive() throws Exception { + assertFalse(this.session.isActive()); + + this.session.initWebSocketSession(this.webSocketSession); + assertTrue(this.session.isActive()); + + this.webSocketSession.setOpen(false); + assertFalse(this.session.isActive()); + } + + @Test + public void initWebSocketSession() throws Exception { + + this.session.initWebSocketSession(this.webSocketSession); + + assertEquals("Open frame not sent", + Collections.singletonList(new TextMessage("o")), this.webSocketSession.getSentMessages()); + + assertEquals(Arrays.asList("schedule"), this.session.heartbeatSchedulingEvents); + verify(this.webSocketHandler).afterConnectionEstablished(this.session); + verifyNoMoreInteractions(this.taskScheduler, this.webSocketHandler); + } + + @Test + public void handleMessageEmptyPayload() throws Exception { + this.session.handleMessage(new TextMessage(""), this.webSocketSession); + verifyNoMoreInteractions(this.webSocketHandler); + } + + @Test + public void handleMessage() throws Exception { + + TextMessage message = new TextMessage("[\"x\"]"); + this.session.handleMessage(message, this.webSocketSession); + + verify(this.webSocketHandler).handleMessage(this.session, new TextMessage("x")); + verifyNoMoreInteractions(this.webSocketHandler); + } + + @Test + public void handleMessageBadData() throws Exception { + TextMessage message = new TextMessage("[\"x]"); + this.session.handleMessage(message, this.webSocketSession); + + this.session.isClosed(); + verify(this.webSocketHandler).handleTransportError(same(this.session), any(IOException.class)); + verifyNoMoreInteractions(this.webSocketHandler); + } + + @Test + public void sendMessageInternal() throws Exception { + + this.session.initWebSocketSession(this.webSocketSession); + this.session.sendMessageInternal("x"); + + assertEquals(Arrays.asList(new TextMessage("o"), new TextMessage("a[\"x\"]")), + this.webSocketSession.getSentMessages()); + + assertEquals(Arrays.asList("schedule", "cancel", "schedule"), this.session.heartbeatSchedulingEvents); + } + + @Test + public void disconnect() throws Exception { + + this.session.initWebSocketSession(this.webSocketSession); + this.session.close(CloseStatus.NOT_ACCEPTABLE); + + assertEquals(CloseStatus.NOT_ACCEPTABLE, this.webSocketSession.getCloseStatus()); + } + + + static class TestWebSocketServerSockJsSession extends WebSocketServerSockJsSession { + + private final List heartbeatSchedulingEvents = new ArrayList<>(); + + public TestWebSocketServerSockJsSession(SockJsConfiguration config, WebSocketHandler handler) { + super("1", config, handler); + } + + @Override + protected void scheduleHeartbeat() { + this.heartbeatSchedulingEvents.add("schedule"); + } + + @Override + protected void cancelHeartbeat() { + this.heartbeatSchedulingEvents.add("cancel"); + } + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/support/BeanCreatingHandlerProviderTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/support/BeanCreatingHandlerProviderTests.java new file mode 100644 index 0000000000..0e66bbad9b --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/support/BeanCreatingHandlerProviderTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.support; + +import org.junit.Test; +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.junit.Assert.*; + + + +/** + * Test fixture for {@link BeanCreatingHandlerProvider}. + * + * @author Rossen Stoyanchev + */ +public class BeanCreatingHandlerProviderTests { + + + @Test + public void getHandlerSimpleInstantiation() { + + BeanCreatingHandlerProvider provider = + new BeanCreatingHandlerProvider(SimpleEchoHandler.class); + + assertNotNull(provider.getHandler()); + } + + @Test + public void getHandlerWithBeanFactory() { + + @SuppressWarnings("resource") + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(Config.class); + + BeanCreatingHandlerProvider provider = + new BeanCreatingHandlerProvider(EchoHandler.class); + provider.setBeanFactory(context.getBeanFactory()); + + assertNotNull(provider.getHandler()); + } + + @Test(expected=BeanInstantiationException.class) + public void getHandlerNoBeanFactory() { + + BeanCreatingHandlerProvider provider = + new BeanCreatingHandlerProvider(EchoHandler.class); + + provider.getHandler(); + } + + + @Configuration + static class Config { + + @Bean + public EchoService echoService() { + return new EchoService(); + } + } + + public static class SimpleEchoHandler { + } + + private static class EchoHandler { + + @SuppressWarnings("unused") + private final EchoService service; + + @Autowired + public EchoHandler(EchoService service) { + this.service = service; + } + } + + private static class EchoService { } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/support/ExceptionWebSocketHandlerDecoratorTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/support/ExceptionWebSocketHandlerDecoratorTests.java new file mode 100644 index 0000000000..f1d7d86667 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/support/ExceptionWebSocketHandlerDecoratorTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.support; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketHandler; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Test fixture for {@link ExceptionWebSocketHandlerDecorator}. + * + * @author Rossen Stoyanchev + */ +public class ExceptionWebSocketHandlerDecoratorTests { + + private TestWebSocketSession session; + + private ExceptionWebSocketHandlerDecorator decorator; + + private WebSocketHandler delegate; + + + @Before + public void setup() { + + this.delegate = mock(WebSocketHandler.class); + this.decorator = new ExceptionWebSocketHandlerDecorator(this.delegate); + + this.session = new TestWebSocketSession(); + this.session.setOpen(true); + } + + @Test + public void afterConnectionEstablished() throws Exception { + + doThrow(new IllegalStateException("error")) + .when(this.delegate).afterConnectionEstablished(this.session); + + this.decorator.afterConnectionEstablished(this.session); + + assertEquals(CloseStatus.SERVER_ERROR, this.session.getCloseStatus()); + } + + @Test + public void handleMessage() throws Exception { + + TextMessage message = new TextMessage("payload"); + + doThrow(new IllegalStateException("error")) + .when(this.delegate).handleMessage(this.session, message); + + this.decorator.handleMessage(this.session, message); + + assertEquals(CloseStatus.SERVER_ERROR, this.session.getCloseStatus()); + } + + @Test + public void handleTransportError() throws Exception { + + Exception exception = new Exception("transport error"); + + doThrow(new IllegalStateException("error")) + .when(this.delegate).handleTransportError(this.session, exception); + + this.decorator.handleTransportError(this.session, exception); + + assertEquals(CloseStatus.SERVER_ERROR, this.session.getCloseStatus()); + } + + @Test + public void afterConnectionClosed() throws Exception { + + CloseStatus closeStatus = CloseStatus.NORMAL; + + doThrow(new IllegalStateException("error")) + .when(this.delegate).afterConnectionClosed(this.session, closeStatus); + + this.decorator.afterConnectionClosed(this.session, closeStatus); + + assertNull(this.session.getCloseStatus()); + } + +} \ No newline at end of file diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/support/PerConnectionWebSocketHandlerTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/support/PerConnectionWebSocketHandlerTests.java new file mode 100644 index 0000000000..76cefcb6d3 --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/support/PerConnectionWebSocketHandlerTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.support; + +import org.junit.Test; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.adapter.WebSocketHandlerAdapter; + +import static org.junit.Assert.*; + +/** + * Test fixture for {@link PerConnectionWebSocketHandler}. + * + * @author Rossen Stoyanchev + */ +public class PerConnectionWebSocketHandlerTests { + + + @Test + public void afterConnectionEstablished() throws Exception { + + @SuppressWarnings("resource") + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.refresh(); + + EchoHandler.reset(); + PerConnectionWebSocketHandler handler = new PerConnectionWebSocketHandler(EchoHandler.class); + handler.setBeanFactory(context.getBeanFactory()); + + WebSocketSession session = new TestWebSocketSession(); + handler.afterConnectionEstablished(session); + + assertEquals(1, EchoHandler.initCount); + assertEquals(0, EchoHandler.destroyCount); + + handler.afterConnectionClosed(session, CloseStatus.NORMAL); + + assertEquals(1, EchoHandler.initCount); + assertEquals(1, EchoHandler.destroyCount); + } + + + public static class EchoHandler extends WebSocketHandlerAdapter implements DisposableBean { + + private static int initCount; + + private static int destroyCount; + + + public EchoHandler() { + initCount++; + } + + @Override + public void destroy() throws Exception { + destroyCount++; + } + + public static void reset() { + initCount = 0; + destroyCount = 0; + } + } + +} diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/support/TestWebSocketSession.java b/spring-websocket/src/test/java/org/springframework/web/socket/support/TestWebSocketSession.java new file mode 100644 index 0000000000..d740d2ed1c --- /dev/null +++ b/spring-websocket/src/test/java/org/springframework/web/socket/support/TestWebSocketSession.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2013 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 org.springframework.web.socket.support; + +import java.io.IOException; +import java.net.URI; +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.WebSocketMessage; +import org.springframework.web.socket.WebSocketSession; + + +/** + * A {@link WebSocketSession} for use in tests. + * + * @author Rossen Stoyanchev + */ +public class TestWebSocketSession implements WebSocketSession { + + private String id; + + private URI uri; + + private boolean secure; + + private Principal principal; + + private String remoteHostName; + + private String remoteAddress; + + private boolean open; + + private final List> messages = new ArrayList<>(); + + private CloseStatus status; + + + /** + * @return the id + */ + @Override + public String getId() { + return id; + } + + /** + * @param id the id to set + */ + public void setId(String id) { + this.id = id; + } + + /** + * @return the uri + */ + @Override + public URI getUri() { + return uri; + } + + /** + * @param uri the uri to set + */ + public void setUri(URI uri) { + this.uri = uri; + } + + /** + * @return the secure + */ + @Override + public boolean isSecure() { + return secure; + } + + /** + * @param secure the secure to set + */ + public void setSecure(boolean secure) { + this.secure = secure; + } + + /** + * @return the principal + */ + @Override + public Principal getPrincipal() { + return principal; + } + + /** + * @param principal the principal to set + */ + public void setPrincipal(Principal principal) { + this.principal = principal; + } + + /** + * @return the remoteHostName + */ + @Override + public String getRemoteHostName() { + return remoteHostName; + } + + /** + * @param remoteHostName the remoteHostName to set + */ + public void setRemoteHostName(String remoteHostName) { + this.remoteHostName = remoteHostName; + } + + /** + * @return the remoteAddress + */ + @Override + public String getRemoteAddress() { + return remoteAddress; + } + + /** + * @param remoteAddress the remoteAddress to set + */ + public void setRemoteAddress(String remoteAddress) { + this.remoteAddress = remoteAddress; + } + + /** + * @return the open + */ + @Override + public boolean isOpen() { + return open; + } + + /** + * @param open the open to set + */ + public void setOpen(boolean open) { + this.open = open; + } + + public List> getSentMessages() { + return this.messages; + } + + public CloseStatus getCloseStatus() { + return this.status; + } + + @Override + public void sendMessage(WebSocketMessage message) throws IOException { + this.messages.add(message); + } + + @Override + public void close() throws IOException { + this.open = false; + } + + @Override + public void close(CloseStatus status) throws IOException { + this.open = false; + this.status = status; + } + +}