[Java] Plumb RxJava through client (#3148)

This commit is contained in:
BrennanConroy 2018-10-23 10:52:26 -07:00 committed by GitHub
parent 0f5fc5b912
commit f7d10bec02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 242 additions and 217 deletions

View File

@ -7,10 +7,11 @@ import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import io.reactivex.Single;
import io.reactivex.subjects.SingleSubject;
import okhttp3.Call; import okhttp3.Call;
import okhttp3.Callback; import okhttp3.Callback;
import okhttp3.Cookie; import okhttp3.Cookie;
@ -78,7 +79,7 @@ final class DefaultHttpClient extends HttpClient {
} }
@Override @Override
public CompletableFuture<HttpResponse> send(HttpRequest httpRequest) { public Single<HttpResponse> send(HttpRequest httpRequest) {
Request.Builder requestBuilder = new Request.Builder().url(httpRequest.getUrl()); Request.Builder requestBuilder = new Request.Builder().url(httpRequest.getUrl());
switch (httpRequest.getMethod()) { switch (httpRequest.getMethod()) {
@ -100,24 +101,24 @@ final class DefaultHttpClient extends HttpClient {
Request request = requestBuilder.build(); Request request = requestBuilder.build();
CompletableFuture<HttpResponse> responseFuture = new CompletableFuture<>(); SingleSubject<HttpResponse> responseSubject = SingleSubject.create();
client.newCall(request).enqueue(new Callback() { client.newCall(request).enqueue(new Callback() {
@Override @Override
public void onFailure(Call call, IOException e) { public void onFailure(Call call, IOException e) {
responseFuture.completeExceptionally(e.getCause()); responseSubject.onError(e.getCause());
} }
@Override @Override
public void onResponse(Call call, Response response) throws IOException { public void onResponse(Call call, Response response) throws IOException {
try (ResponseBody body = response.body()) { try (ResponseBody body = response.body()) {
HttpResponse httpResponse = new HttpResponse(response.code(), response.message(), body.string()); HttpResponse httpResponse = new HttpResponse(response.code(), response.message(), body.string());
responseFuture.complete(httpResponse); responseSubject.onSuccess(httpResponse);
} }
} }
}); });
return responseFuture; return responseSubject;
} }
@Override @Override

View File

@ -5,7 +5,8 @@ package com.microsoft.signalr;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture;
import io.reactivex.Single;
class HttpRequest { class HttpRequest {
private String method; private String method;
@ -74,46 +75,46 @@ class HttpResponse {
} }
abstract class HttpClient { abstract class HttpClient {
public CompletableFuture<HttpResponse> get(String url) { public Single<HttpResponse> get(String url) {
HttpRequest request = new HttpRequest(); HttpRequest request = new HttpRequest();
request.setUrl(url); request.setUrl(url);
request.setMethod("GET"); request.setMethod("GET");
return this.send(request); return this.send(request);
} }
public CompletableFuture<HttpResponse> get(String url, HttpRequest options) { public Single<HttpResponse> get(String url, HttpRequest options) {
options.setUrl(url); options.setUrl(url);
options.setMethod("GET"); options.setMethod("GET");
return this.send(options); return this.send(options);
} }
public CompletableFuture<HttpResponse> post(String url) { public Single<HttpResponse> post(String url) {
HttpRequest request = new HttpRequest(); HttpRequest request = new HttpRequest();
request.setUrl(url); request.setUrl(url);
request.setMethod("POST"); request.setMethod("POST");
return this.send(request); return this.send(request);
} }
public CompletableFuture<HttpResponse> post(String url, HttpRequest options) { public Single<HttpResponse> post(String url, HttpRequest options) {
options.setUrl(url); options.setUrl(url);
options.setMethod("POST"); options.setMethod("POST");
return this.send(options); return this.send(options);
} }
public CompletableFuture<HttpResponse> delete(String url) { public Single<HttpResponse> delete(String url) {
HttpRequest request = new HttpRequest(); HttpRequest request = new HttpRequest();
request.setUrl(url); request.setUrl(url);
request.setMethod("DELETE"); request.setMethod("DELETE");
return this.send(request); return this.send(request);
} }
public CompletableFuture<HttpResponse> delete(String url, HttpRequest options) { public Single<HttpResponse> delete(String url, HttpRequest options) {
options.setUrl(url); options.setUrl(url);
options.setMethod("DELETE"); options.setMethod("DELETE");
return this.send(options); return this.send(options);
} }
public abstract CompletableFuture<HttpResponse> send(HttpRequest request); public abstract Single<HttpResponse> send(HttpRequest request);
public abstract WebSocketWrapper createWebSocket(String url, Map<String, String> headers); public abstract WebSocketWrapper createWebSocket(String url, Map<String, String> headers);
} }

View File

@ -22,6 +22,8 @@ import org.slf4j.LoggerFactory;
import io.reactivex.Completable; import io.reactivex.Completable;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.subjects.CompletableSubject;
import io.reactivex.subjects.SingleSubject;
public class HubConnection { public class HubConnection {
private static final String RECORD_SEPARATOR = "\u001e"; private static final String RECORD_SEPARATOR = "\u001e";
@ -49,7 +51,7 @@ public class HubConnection {
private long keepAliveInterval = 15*1000; private long keepAliveInterval = 15*1000;
private long serverTimeout = 30*1000; private long serverTimeout = 30*1000;
private long tickRate = 1000; private long tickRate = 1000;
private CompletableFuture<Void> handshakeResponseFuture; private CompletableSubject handshakeResponseSubject;
private long handshakeResponseTimeout = 15*1000; private long handshakeResponseTimeout = 15*1000;
private final Logger logger = LoggerFactory.getLogger(HubConnection.class); private final Logger logger = LoggerFactory.getLogger(HubConnection.class);
@ -140,18 +142,18 @@ public class HubConnection {
handshakeResponse = HandshakeProtocol.parseHandshakeResponse(handshakeResponseString); handshakeResponse = HandshakeProtocol.parseHandshakeResponse(handshakeResponseString);
} catch (RuntimeException ex) { } catch (RuntimeException ex) {
RuntimeException exception = new RuntimeException("An invalid handshake response was received from the server.", ex); RuntimeException exception = new RuntimeException("An invalid handshake response was received from the server.", ex);
handshakeResponseFuture.completeExceptionally(exception); handshakeResponseSubject.onError(exception);
throw exception; throw exception;
} }
if (handshakeResponse.getHandshakeError() != null) { if (handshakeResponse.getHandshakeError() != null) {
String errorMessage = "Error in handshake " + handshakeResponse.getHandshakeError(); String errorMessage = "Error in handshake " + handshakeResponse.getHandshakeError();
logger.error(errorMessage); logger.error(errorMessage);
RuntimeException exception = new RuntimeException(errorMessage); RuntimeException exception = new RuntimeException(errorMessage);
handshakeResponseFuture.completeExceptionally(exception); handshakeResponseSubject.onError(exception);
throw exception; throw exception;
} }
handshakeReceived = true; handshakeReceived = true;
handshakeResponseFuture.complete(null); handshakeResponseSubject.onComplete();
payload = payload.substring(handshakeLength); payload = payload.substring(handshakeLength);
// The payload only contained the handshake response so we can return. // The payload only contained the handshake response so we can return.
@ -206,15 +208,21 @@ public class HubConnection {
private void timeoutHandshakeResponse(long timeout, TimeUnit unit) { private void timeoutHandshakeResponse(long timeout, TimeUnit unit) {
ScheduledExecutorService scheduledThreadPool = Executors.newSingleThreadScheduledExecutor(); ScheduledExecutorService scheduledThreadPool = Executors.newSingleThreadScheduledExecutor();
scheduledThreadPool.schedule(() -> handshakeResponseFuture.completeExceptionally( scheduledThreadPool.schedule(() -> {
new TimeoutException("Timed out waiting for the server to respond to the handshake message.")), timeout, unit); // If onError is called on a completed subject the global error handler is called
if (!(handshakeResponseSubject.hasComplete() || handshakeResponseSubject.hasThrowable()))
{
handshakeResponseSubject.onError(
new TimeoutException("Timed out waiting for the server to respond to the handshake message."));
}
}, timeout, unit);
} }
private CompletableFuture<NegotiateResponse> handleNegotiate(String url) { private Single<NegotiateResponse> handleNegotiate(String url) {
HttpRequest request = new HttpRequest(); HttpRequest request = new HttpRequest();
request.addHeaders(this.headers); request.addHeaders(this.headers);
return httpClient.post(Negotiate.resolveNegotiateUrl(url), request).thenCompose((response) -> { return httpClient.post(Negotiate.resolveNegotiateUrl(url), request).map((response) -> {
if (response.getStatusCode() != 200) { if (response.getStatusCode() != 200) {
throw new RuntimeException(String.format("Unexpected status code returned from negotiate: %d %s.", response.getStatusCode(), response.getStatusText())); throw new RuntimeException(String.format("Unexpected status code returned from negotiate: %d %s.", response.getStatusCode(), response.getStatusText()));
} }
@ -233,7 +241,7 @@ public class HubConnection {
this.headers.put("Authorization", "Bearer " + token); this.headers.put("Authorization", "Bearer " + token);
} }
return CompletableFuture.completedFuture(negotiateResponse); return negotiateResponse;
}); });
} }
@ -256,25 +264,27 @@ public class HubConnection {
return Completable.complete(); return Completable.complete();
} }
handshakeResponseFuture = new CompletableFuture<>(); handshakeResponseSubject = CompletableSubject.create();
handshakeReceived = false; handshakeReceived = false;
CompletableFuture<Void> tokenFuture = new CompletableFuture<>(); CompletableSubject tokenCompletable = CompletableSubject.create();
accessTokenProvider.subscribe(token -> { accessTokenProvider.subscribe(token -> {
if (token != null && !token.isEmpty()) { if (token != null && !token.isEmpty()) {
this.headers.put("Authorization", "Bearer " + token); this.headers.put("Authorization", "Bearer " + token);
} }
tokenFuture.complete(null); tokenCompletable.onComplete();
}); });
stopError = null; stopError = null;
CompletableFuture<String> negotiate = null; Single<String> negotiate = null;
if (!skipNegotiate) { if (!skipNegotiate) {
negotiate = tokenFuture.thenCompose((v) -> startNegotiate(baseUrl, 0)); negotiate = tokenCompletable.andThen(Single.defer(() -> startNegotiate(baseUrl, 0)));
} else { } else {
negotiate = tokenFuture.thenCompose((v) -> CompletableFuture.completedFuture(baseUrl)); negotiate = tokenCompletable.andThen(Single.defer(() -> Single.just(baseUrl)));
} }
return Completable.fromFuture(negotiate.thenCompose(url -> { CompletableSubject start = CompletableSubject.create();
negotiate.flatMapCompletable(url -> {
logger.debug("Starting HubConnection."); logger.debug("Starting HubConnection.");
if (transport == null) { if (transport == null) {
transport = new WebSocketTransport(headers, httpClient); transport = new WebSocketTransport(headers, httpClient);
@ -283,16 +293,17 @@ public class HubConnection {
transport.setOnReceive(this.callback); transport.setOnReceive(this.callback);
transport.setOnClose((message) -> stopConnection(message)); transport.setOnClose((message) -> stopConnection(message));
return transport.start(url).thenCompose((future) -> { return transport.start(url).andThen(Completable.defer(() -> {
String handshake = HandshakeProtocol.createHandshakeRequestMessage( String handshake = HandshakeProtocol.createHandshakeRequestMessage(
new HandshakeRequestMessage(protocol.getName(), protocol.getVersion())); new HandshakeRequestMessage(protocol.getName(), protocol.getVersion()));
return transport.send(handshake).thenCompose((innerFuture) -> {
return transport.send(handshake).andThen(Completable.defer(() -> {
timeoutHandshakeResponse(handshakeResponseTimeout, TimeUnit.MILLISECONDS); timeoutHandshakeResponse(handshakeResponseTimeout, TimeUnit.MILLISECONDS);
return handshakeResponseFuture.thenRun(() -> { return handshakeResponseSubject.andThen(Completable.defer(() -> {
hubConnectionStateLock.lock(); hubConnectionStateLock.lock();
try { try {
hubConnectionState = HubConnectionState.CONNECTED;
connectionState = new ConnectionState(this); connectionState = new ConnectionState(this);
hubConnectionState = HubConnectionState.CONNECTED;
logger.info("HubConnection started."); logger.info("HubConnection started.");
resetServerTimeout(); resetServerTimeout();
@ -320,18 +331,23 @@ public class HubConnection {
} finally { } finally {
hubConnectionStateLock.unlock(); hubConnectionStateLock.unlock();
} }
});
}); return Completable.complete();
}); }));
})); }));
}));
// subscribe makes this a "hot" completable so this runs immediately
}).subscribeWith(start);
return start;
} }
private CompletableFuture<String> startNegotiate(String url, int negotiateAttempts) { private Single<String> startNegotiate(String url, int negotiateAttempts) {
if (hubConnectionState != HubConnectionState.DISCONNECTED) { if (hubConnectionState != HubConnectionState.DISCONNECTED) {
return CompletableFuture.completedFuture(null); return Single.just(null);
} }
return handleNegotiate(url).thenCompose((response) -> { return handleNegotiate(url).flatMap(response -> {
if (response.getRedirectUrl() != null && negotiateAttempts >= MAX_NEGOTIATE_ATTEMPTS) { if (response.getRedirectUrl() != null && negotiateAttempts >= MAX_NEGOTIATE_ATTEMPTS) {
throw new RuntimeException("Negotiate redirection limit exceeded."); throw new RuntimeException("Negotiate redirection limit exceeded.");
} }
@ -350,7 +366,7 @@ public class HubConnection {
} }
} }
return CompletableFuture.completedFuture(finalUrl); return Single.just(finalUrl);
} }
return startNegotiate(response.getRedirectUrl(), negotiateAttempts + 1); return startNegotiate(response.getRedirectUrl(), negotiateAttempts + 1);
@ -363,11 +379,11 @@ public class HubConnection {
* @param errorMessage An error message if the connected needs to be stopped because of an error. * @param errorMessage An error message if the connected needs to be stopped because of an error.
* @return A Completable that completes when the connection has been stopped. * @return A Completable that completes when the connection has been stopped.
*/ */
private CompletableFuture<Void> stop(String errorMessage) { private Completable stop(String errorMessage) {
hubConnectionStateLock.lock(); hubConnectionStateLock.lock();
try { try {
if (hubConnectionState == HubConnectionState.DISCONNECTED) { if (hubConnectionState == HubConnectionState.DISCONNECTED) {
return CompletableFuture.completedFuture(null); return Completable.complete();
} }
if (errorMessage != null) { if (errorMessage != null) {
@ -389,7 +405,7 @@ public class HubConnection {
* @return A Completable that completes when the connection has been stopped. * @return A Completable that completes when the connection has been stopped.
*/ */
public Completable stop() { public Completable stop() {
return Completable.fromFuture(stop(null)); return stop(null);
} }
private void stopConnection(String errorMessage) { private void stopConnection(String errorMessage) {
@ -409,7 +425,7 @@ public class HubConnection {
connectionState = null; connectionState = null;
logger.info("HubConnection stopped."); logger.info("HubConnection stopped.");
hubConnectionState = HubConnectionState.DISCONNECTED; hubConnectionState = HubConnectionState.DISCONNECTED;
handshakeResponseFuture.complete(null); handshakeResponseSubject.onComplete();
} finally { } finally {
hubConnectionStateLock.unlock(); hubConnectionStateLock.unlock();
} }
@ -452,31 +468,27 @@ public class HubConnection {
String id = connectionState.getNextInvocationId(); String id = connectionState.getNextInvocationId();
InvocationMessage invocationMessage = new InvocationMessage(id, method, args); InvocationMessage invocationMessage = new InvocationMessage(id, method, args);
CompletableFuture<T> future = new CompletableFuture<>(); SingleSubject<T> subject = SingleSubject.create();
InvocationRequest irq = new InvocationRequest(returnType, id); InvocationRequest irq = new InvocationRequest(returnType, id);
connectionState.addInvocation(irq); connectionState.addInvocation(irq);
// forward the invocation result or error to the user // forward the invocation result or error to the user
// run continuations on a separate thread // run continuations on a separate thread
CompletableFuture<Object> pendingCall = irq.getPendingCall(); Single<Object> pendingCall = irq.getPendingCall();
pendingCall.whenCompleteAsync((result, error) -> { pendingCall.subscribe(result -> {
if (error == null) { // Primitive types can't be cast with the Class cast function
// Primitive types can't be cast with the Class cast function if (returnType.isPrimitive()) {
if (returnType.isPrimitive()) { subject.onSuccess((T)result);
future.complete((T)result);
} else {
future.complete(returnType.cast(result));
}
} else { } else {
future.completeExceptionally(error); subject.onSuccess(returnType.cast(result));
} }
}); }, error -> subject.onError(error));
// Make sure the actual send is after setting up the future otherwise there is a race // Make sure the actual send is after setting up the callbacks otherwise there is a race
// where the map doesn't have the future yet when the response is returned // where the map doesn't have the callbacks yet when the response is returned
sendHubMessage(invocationMessage); sendHubMessage(invocationMessage);
return Single.fromFuture(future); return subject;
} }
private void sendHubMessage(HubMessage message) { private void sendHubMessage(HubMessage message) {

View File

@ -3,11 +3,14 @@
package com.microsoft.signalr; package com.microsoft.signalr;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CancellationException;
import io.reactivex.Single;
import io.reactivex.subjects.SingleSubject;
class InvocationRequest { class InvocationRequest {
private final Class<?> returnType; private final Class<?> returnType;
private final CompletableFuture<Object> pendingCall = new CompletableFuture<>(); private final SingleSubject<Object> pendingCall = SingleSubject.create();
private final String invocationId; private final String invocationId;
InvocationRequest(Class<?> returnType, String invocationId) { InvocationRequest(Class<?> returnType, String invocationId) {
@ -17,21 +20,21 @@ class InvocationRequest {
public void complete(CompletionMessage completion) { public void complete(CompletionMessage completion) {
if (completion.getResult() != null) { if (completion.getResult() != null) {
pendingCall.complete(completion.getResult()); pendingCall.onSuccess(completion.getResult());
} else { } else {
pendingCall.completeExceptionally(new HubException(completion.getError())); pendingCall.onError(new HubException(completion.getError()));
} }
} }
public void fail(Exception ex) { public void fail(Exception ex) {
pendingCall.completeExceptionally(ex); pendingCall.onError(ex);
} }
public void cancel() { public void cancel() {
pendingCall.cancel(false); pendingCall.onError(new CancellationException("Invocation was canceled."));
} }
public CompletableFuture<Object> getPendingCall() { public Single<Object> getPendingCall() {
return pendingCall; return pendingCall;
} }

View File

@ -4,12 +4,13 @@
package com.microsoft.signalr; package com.microsoft.signalr;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import io.reactivex.Completable;
import io.reactivex.subjects.CompletableSubject;
import okhttp3.Headers; import okhttp3.Headers;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
@ -24,8 +25,8 @@ class OkHttpWebSocketWrapper extends WebSocketWrapper {
private OkHttpClient client; private OkHttpClient client;
private OnReceiveCallBack onReceive; private OnReceiveCallBack onReceive;
private BiConsumer<Integer, String> onClose; private BiConsumer<Integer, String> onClose;
private CompletableFuture<Void> startFuture = new CompletableFuture<>(); private CompletableSubject startSubject = CompletableSubject.create();
private CompletableFuture<Void> closeFuture = new CompletableFuture<>(); private CompletableSubject closeSubject = CompletableSubject.create();
private final Logger logger = LoggerFactory.getLogger(OkHttpWebSocketWrapper.class); private final Logger logger = LoggerFactory.getLogger(OkHttpWebSocketWrapper.class);
@ -36,7 +37,7 @@ class OkHttpWebSocketWrapper extends WebSocketWrapper {
} }
@Override @Override
public CompletableFuture<Void> start() { public Completable start() {
Headers.Builder headerBuilder = new Headers.Builder(); Headers.Builder headerBuilder = new Headers.Builder();
for (String key : headers.keySet()) { for (String key : headers.keySet()) {
headerBuilder.add(key, headers.get(key)); headerBuilder.add(key, headers.get(key));
@ -48,19 +49,19 @@ class OkHttpWebSocketWrapper extends WebSocketWrapper {
.build(); .build();
this.websocketClient = client.newWebSocket(request, new SignalRWebSocketListener()); this.websocketClient = client.newWebSocket(request, new SignalRWebSocketListener());
return startFuture; return startSubject;
} }
@Override @Override
public CompletableFuture<Void> stop() { public Completable stop() {
websocketClient.close(1000, "HubConnection stopped."); websocketClient.close(1000, "HubConnection stopped.");
return closeFuture; return closeSubject;
} }
@Override @Override
public CompletableFuture<Void> send(String message) { public Completable send(String message) {
websocketClient.send(message); websocketClient.send(message);
return CompletableFuture.completedFuture(null); return Completable.complete();
} }
@Override @Override
@ -76,7 +77,7 @@ class OkHttpWebSocketWrapper extends WebSocketWrapper {
private class SignalRWebSocketListener extends WebSocketListener { private class SignalRWebSocketListener extends WebSocketListener {
@Override @Override
public void onOpen(WebSocket webSocket, Response response) { public void onOpen(WebSocket webSocket, Response response) {
startFuture.complete(null); startSubject.onComplete();
} }
@Override @Override
@ -92,25 +93,25 @@ class OkHttpWebSocketWrapper extends WebSocketWrapper {
@Override @Override
public void onClosing(WebSocket webSocket, int code, String reason) { public void onClosing(WebSocket webSocket, int code, String reason) {
onClose.accept(code, reason); onClose.accept(code, reason);
closeFuture.complete(null); closeSubject.onComplete();
checkStartFailure(); checkStartFailure();
} }
@Override @Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) { public void onFailure(WebSocket webSocket, Throwable t, Response response) {
logger.error("Websocket closed from an error: {}.", t.getMessage()); logger.error("Websocket closed from an error: {}.", t.getMessage());
closeFuture.completeExceptionally(new RuntimeException(t)); closeSubject.onError(new RuntimeException(t));
onClose.accept(null, t.getMessage()); onClose.accept(null, t.getMessage());
checkStartFailure(); checkStartFailure();
} }
private void checkStartFailure() { private void checkStartFailure() {
// If the start future hasn't completed yet, then we need to complete it // If the start task hasn't completed yet, then we need to complete it
// exceptionally. // exceptionally.
if (!startFuture.isDone()) { if (!startSubject.hasComplete()) {
String errorMessage = "There was an error starting the Websockets transport."; String errorMessage = "There was an error starting the Websockets transport.";
logger.error("Websocket closed from an error: {}.", errorMessage); logger.error("Websocket closed from an error: {}.", errorMessage);
startFuture.completeExceptionally(new RuntimeException(errorMessage)); startSubject.onError(new RuntimeException(errorMessage));
} }
} }
} }

View File

@ -3,14 +3,15 @@
package com.microsoft.signalr; package com.microsoft.signalr;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer; import java.util.function.Consumer;
import io.reactivex.Completable;
interface Transport { interface Transport {
CompletableFuture<Void> start(String url); Completable start(String url);
CompletableFuture<Void> send(String message); Completable send(String message);
void setOnReceive(OnReceiveCallBack callback); void setOnReceive(OnReceiveCallBack callback);
void onReceive(String message); void onReceive(String message);
void setOnClose(Consumer<String> onCloseCallback); void setOnClose(Consumer<String> onCloseCallback);
CompletableFuture<Void> stop(); Completable stop();
} }

View File

@ -4,12 +4,13 @@
package com.microsoft.signalr; package com.microsoft.signalr;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import io.reactivex.Completable;
class WebSocketTransport implements Transport { class WebSocketTransport implements Transport {
private WebSocketWrapper webSocketClient; private WebSocketWrapper webSocketClient;
private OnReceiveCallBack onReceiveCallBack; private OnReceiveCallBack onReceiveCallBack;
@ -46,7 +47,7 @@ class WebSocketTransport implements Transport {
} }
@Override @Override
public CompletableFuture<Void> start(String url) { public Completable start(String url) {
this.url = formatUrl(url); this.url = formatUrl(url);
logger.debug("Starting Websocket connection."); logger.debug("Starting Websocket connection.");
this.webSocketClient = client.createWebSocket(this.url, this.headers); this.webSocketClient = client.createWebSocket(this.url, this.headers);
@ -57,11 +58,11 @@ class WebSocketTransport implements Transport {
} }
}); });
return webSocketClient.start().thenRun(() -> logger.info("WebSocket transport connected to: {}.", this.url)); return webSocketClient.start().doOnComplete(() -> logger.info("WebSocket transport connected to: {}.", this.url));
} }
@Override @Override
public CompletableFuture<Void> send(String message) { public Completable send(String message) {
return webSocketClient.send(message); return webSocketClient.send(message);
} }
@ -82,8 +83,8 @@ class WebSocketTransport implements Transport {
} }
@Override @Override
public CompletableFuture<Void> stop() { public Completable stop() {
return webSocketClient.stop().whenComplete((i, j) -> logger.info("WebSocket connection stopped.")); return webSocketClient.stop().doOnEvent(t -> logger.info("WebSocket connection stopped."));
} }
void onClose(int code, String reason) { void onClose(int code, String reason) {

View File

@ -3,15 +3,16 @@
package com.microsoft.signalr; package com.microsoft.signalr;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import io.reactivex.Completable;
abstract class WebSocketWrapper { abstract class WebSocketWrapper {
public abstract CompletableFuture<Void> start(); public abstract Completable start();
public abstract CompletableFuture<Void> stop(); public abstract Completable stop();
public abstract CompletableFuture<Void> send(String message); public abstract Completable send(String message);
public abstract void setOnReceive(OnReceiveCallBack onReceive); public abstract void setOnReceive(OnReceiveCallBack onReceive);

View File

@ -7,7 +7,6 @@ import static org.junit.jupiter.api.Assertions.*;
import java.util.List; import java.util.List;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
@ -16,8 +15,8 @@ import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import io.reactivex.Completable;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.subjects.SingleSubject;
class HubConnectionTest { class HubConnectionTest {
private static final String RECORD_SEPARATOR = "\u001e"; private static final String RECORD_SEPARATOR = "\u001e";
@ -25,7 +24,7 @@ class HubConnectionTest {
@Test @Test
public void checkHubConnectionState() { public void checkHubConnectionState() {
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com"); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com");
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
hubConnection.stop(); hubConnection.stop();
@ -36,7 +35,7 @@ class HubConnectionTest {
public void transportCloseTriggersStopInHubConnection() { public void transportCloseTriggersStopInHubConnection() {
MockTransport mockTransport = new MockTransport(); MockTransport mockTransport = new MockTransport();
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport);
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
mockTransport.stop(); mockTransport.stop();
@ -54,7 +53,7 @@ class HubConnectionTest {
message.set(error.getMessage()); message.set(error.getMessage());
}); });
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
mockTransport.stopWithError(errorMessage); mockTransport.stopWithError(errorMessage);
assertEquals(errorMessage, message.get()); assertEquals(errorMessage, message.get());
@ -69,10 +68,9 @@ class HubConnectionTest {
.shouldSkipNegotiate(true) .shouldSkipNegotiate(true)
.withHandshakeResponseTimeout(100) .withHandshakeResponseTimeout(100)
.build(); .build();
Throwable exception = assertThrows(RuntimeException.class, () -> hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS)); Throwable exception = assertThrows(RuntimeException.class, () -> hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait());
assertEquals(ExecutionException.class, exception.getCause().getClass()); assertEquals(TimeoutException.class, exception.getCause().getClass());
assertEquals(TimeoutException.class, exception.getCause().getCause().getClass()); assertEquals("Timed out waiting for the server to respond to the handshake message.", exception.getCause().getMessage());
assertEquals(exception.getCause().getCause().getMessage(), "Timed out waiting for the server to respond to the handshake message.");
assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState());
} }
@ -81,7 +79,7 @@ class HubConnectionTest {
Transport mockTransport = new MockTransport(); Transport mockTransport = new MockTransport();
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
hubConnection.stop(); hubConnection.stop();
@ -93,7 +91,7 @@ class HubConnectionTest {
MockTransport mockTransport = new MockTransport(); MockTransport mockTransport = new MockTransport();
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
@ -108,6 +106,7 @@ class HubConnectionTest {
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport);
hubConnection.start(); hubConnection.start();
mockTransport.getStartTask().timeout(1, TimeUnit.SECONDS).blockingAwait();
Throwable exception = assertThrows(RuntimeException.class, () -> mockTransport.receiveMessage("{" + RECORD_SEPARATOR)); Throwable exception = assertThrows(RuntimeException.class, () -> mockTransport.receiveMessage("{" + RECORD_SEPARATOR));
assertEquals("An invalid handshake response was received from the server.", exception.getMessage()); assertEquals("An invalid handshake response was received from the server.", exception.getMessage());
@ -120,6 +119,7 @@ class HubConnectionTest {
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport);
hubConnection.start(); hubConnection.start();
mockTransport.getStartTask().timeout(1, TimeUnit.SECONDS).blockingAwait();
Throwable exception = assertThrows(RuntimeException.class, () -> mockTransport.receiveMessage("{\"error\":\"Requested protocol 'messagepack' is not available.\"}" + RECORD_SEPARATOR)); Throwable exception = assertThrows(RuntimeException.class, () -> mockTransport.receiveMessage("{\"error\":\"Requested protocol 'messagepack' is not available.\"}" + RECORD_SEPARATOR));
assertEquals("Error in handshake Requested protocol 'messagepack' is not available.", exception.getMessage()); assertEquals("Error in handshake Requested protocol 'messagepack' is not available.", exception.getMessage());
} }
@ -136,7 +136,7 @@ class HubConnectionTest {
assertEquals(Double.valueOf(0), value.get()); assertEquals(Double.valueOf(0), value.get());
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
String message = mockTransport.getSentMessages()[0]; String message = mockTransport.getSentMessages()[0];
String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR;
@ -160,7 +160,7 @@ class HubConnectionTest {
assertEquals(Double.valueOf(0), value.get()); assertEquals(Double.valueOf(0), value.get());
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
String message = mockTransport.getSentMessages()[0]; String message = mockTransport.getSentMessages()[0];
String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR;
@ -187,7 +187,7 @@ class HubConnectionTest {
assertEquals(Double.valueOf(0), value.get()); assertEquals(Double.valueOf(0), value.get());
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
String message = mockTransport.getSentMessages()[0]; String message = mockTransport.getSentMessages()[0];
String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR;
@ -212,7 +212,7 @@ class HubConnectionTest {
assertEquals(Double.valueOf(0), value.get()); assertEquals(Double.valueOf(0), value.get());
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
String message = mockTransport.getSentMessages()[0]; String message = mockTransport.getSentMessages()[0];
String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR;
@ -241,7 +241,7 @@ class HubConnectionTest {
assertEquals(Double.valueOf(0), value.get()); assertEquals(Double.valueOf(0), value.get());
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
String message = mockTransport.getSentMessages()[0]; String message = mockTransport.getSentMessages()[0];
String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR;
@ -273,7 +273,7 @@ class HubConnectionTest {
assertEquals(Double.valueOf(0), value.get()); assertEquals(Double.valueOf(0), value.get());
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
String message = mockTransport.getSentMessages()[0]; String message = mockTransport.getSentMessages()[0];
String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR;
@ -308,7 +308,7 @@ class HubConnectionTest {
assertEquals(Double.valueOf(0), value.get()); assertEquals(Double.valueOf(0), value.get());
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
String message = mockTransport.getSentMessages()[0]; String message = mockTransport.getSentMessages()[0];
String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; String expectedHanshakeRequest = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR;
@ -336,7 +336,7 @@ class HubConnectionTest {
assertEquals(Double.valueOf(0), value.get()); assertEquals(Double.valueOf(0), value.get());
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
try { try {
mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[]}" + RECORD_SEPARATOR); mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[]}" + RECORD_SEPARATOR);
@ -360,7 +360,7 @@ class HubConnectionTest {
hubConnection.on("add", action, Double.class); hubConnection.on("add", action, Double.class);
assertEquals(Double.valueOf(0), value.get()); assertEquals(Double.valueOf(0), value.get());
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
mockTransport.receiveMessage("{\"type\":1,\"target\":\"add\",\"arguments\":[12]}" + RECORD_SEPARATOR); mockTransport.receiveMessage("{\"type\":1,\"target\":\"add\",\"arguments\":[12]}" + RECORD_SEPARATOR);
// Confirming that our handler was called and the correct message was passed in. // Confirming that our handler was called and the correct message was passed in.
@ -372,7 +372,7 @@ class HubConnectionTest {
MockTransport mockTransport = new MockTransport(); MockTransport mockTransport = new MockTransport();
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
AtomicBoolean done = new AtomicBoolean(); AtomicBoolean done = new AtomicBoolean();
Single<Integer> result = hubConnection.invoke(Integer.class, "echo", "message"); Single<Integer> result = hubConnection.invoke(Integer.class, "echo", "message");
@ -390,7 +390,7 @@ class HubConnectionTest {
MockTransport mockTransport = new MockTransport(); MockTransport mockTransport = new MockTransport();
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
AtomicBoolean doneFirst = new AtomicBoolean(); AtomicBoolean doneFirst = new AtomicBoolean();
AtomicBoolean doneSecond = new AtomicBoolean(); AtomicBoolean doneSecond = new AtomicBoolean();
@ -416,7 +416,7 @@ class HubConnectionTest {
MockTransport mockTransport = new MockTransport(); MockTransport mockTransport = new MockTransport();
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
AtomicBoolean done = new AtomicBoolean(); AtomicBoolean done = new AtomicBoolean();
// int.class is a primitive type and since we use Class.cast to cast an Object to the expected return type // int.class is a primitive type and since we use Class.cast to cast an Object to the expected return type
@ -435,7 +435,7 @@ class HubConnectionTest {
MockTransport mockTransport = new MockTransport(); MockTransport mockTransport = new MockTransport();
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
AtomicBoolean done = new AtomicBoolean(); AtomicBoolean done = new AtomicBoolean();
Single<Integer> result = hubConnection.invoke(int.class, "echo", "message"); Single<Integer> result = hubConnection.invoke(int.class, "echo", "message");
@ -448,11 +448,11 @@ class HubConnectionTest {
try { try {
result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet(); result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet();
assertFalse(true); assertFalse(true);
} catch (Exception ex) { } catch (HubException ex) {
exceptionMessage = ex.getCause().getMessage(); exceptionMessage = ex.getMessage();
} }
assertEquals("com.microsoft.signalr.HubException: There was an error", exceptionMessage); assertEquals("There was an error", exceptionMessage);
} }
@Test @Test
@ -460,7 +460,7 @@ class HubConnectionTest {
MockTransport mockTransport = new MockTransport(); MockTransport mockTransport = new MockTransport();
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
AtomicBoolean done = new AtomicBoolean(); AtomicBoolean done = new AtomicBoolean();
Single<Integer> result = hubConnection.invoke(int.class, "echo", "message"); Single<Integer> result = hubConnection.invoke(int.class, "echo", "message");
@ -469,15 +469,15 @@ class HubConnectionTest {
hubConnection.stop(); hubConnection.stop();
boolean hasException = false; RuntimeException hasException = null;
try { try {
result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet(); result.timeout(1000, TimeUnit.MILLISECONDS).blockingGet();
assertFalse(true); assertFalse(true);
} catch (CancellationException ex) { } catch (CancellationException ex) {
hasException = true; hasException = ex;
} }
assertTrue(hasException); assertEquals("Invocation was canceled.", hasException.getMessage());
} }
@Test @Test
@ -491,7 +491,7 @@ class HubConnectionTest {
value.getAndUpdate((val) -> val + 1); value.getAndUpdate((val) -> val + 1);
}); });
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[]}" + RECORD_SEPARATOR); mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[]}" + RECORD_SEPARATOR);
// Confirming that our handler was called and that the counter property was incremented. // Confirming that our handler was called and that the counter property was incremented.
@ -509,7 +509,7 @@ class HubConnectionTest {
value.set(param); value.set(param);
}, String.class); }, String.class);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"Hello World\"]}" + RECORD_SEPARATOR); mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"Hello World\"]}" + RECORD_SEPARATOR);
hubConnection.send("inc", "Hello World"); hubConnection.send("inc", "Hello World");
@ -533,7 +533,7 @@ class HubConnectionTest {
value2.set(param2); value2.set(param2);
}, String.class, Double.class); }, String.class, Double.class);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"Hello World\", 12]}" + RECORD_SEPARATOR); mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"Hello World\", 12]}" + RECORD_SEPARATOR);
hubConnection.send("inc", "Hello World", 12); hubConnection.send("inc", "Hello World", 12);
@ -561,7 +561,7 @@ class HubConnectionTest {
value3.set(param3); value3.set(param3);
}, String.class, String.class, String.class); }, String.class, String.class, String.class);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"A\", \"B\", \"C\"]}" + RECORD_SEPARATOR); mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"A\", \"B\", \"C\"]}" + RECORD_SEPARATOR);
hubConnection.send("inc", "A", "B", "C"); hubConnection.send("inc", "A", "B", "C");
@ -593,7 +593,7 @@ class HubConnectionTest {
value4.set(param4); value4.set(param4);
}, String.class, String.class, String.class, String.class); }, String.class, String.class, String.class, String.class);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"A\", \"B\", \"C\", \"D\"]}" + RECORD_SEPARATOR); mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"A\", \"B\", \"C\", \"D\"]}" + RECORD_SEPARATOR);
// Confirming that our handler was called and the correct message was passed in. // Confirming that our handler was called and the correct message was passed in.
@ -628,7 +628,7 @@ class HubConnectionTest {
value5.set(param5); value5.set(param5);
}, String.class, String.class, String.class, Boolean.class, Double.class); }, String.class, String.class, String.class, Boolean.class, Double.class);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"A\", \"B\", \"C\",true,12 ]}" + RECORD_SEPARATOR); mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"A\", \"B\", \"C\",true,12 ]}" + RECORD_SEPARATOR);
// Confirming that our handler was called and the correct message was passed in. // Confirming that our handler was called and the correct message was passed in.
@ -667,7 +667,7 @@ class HubConnectionTest {
value6.set(param6); value6.set(param6);
}, String.class, String.class, String.class, Boolean.class, Double.class, String.class); }, String.class, String.class, String.class, Boolean.class, Double.class, String.class);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"A\", \"B\", \"C\",true,12,\"D\"]}" + RECORD_SEPARATOR); mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"A\", \"B\", \"C\",true,12,\"D\"]}" + RECORD_SEPARATOR);
// Confirming that our handler was called and the correct message was passed in. // Confirming that our handler was called and the correct message was passed in.
@ -710,7 +710,7 @@ class HubConnectionTest {
value7.set(param7); value7.set(param7);
}, String.class, String.class, String.class, Boolean.class, Double.class, String.class, String.class); }, String.class, String.class, String.class, Boolean.class, Double.class, String.class, String.class);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"A\", \"B\", \"C\",true,12,\"D\",\"E\"]}" + RECORD_SEPARATOR); mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"A\", \"B\", \"C\",true,12,\"D\",\"E\"]}" + RECORD_SEPARATOR);
// Confirming that our handler was called and the correct message was passed in. // Confirming that our handler was called and the correct message was passed in.
@ -757,7 +757,7 @@ class HubConnectionTest {
value8.set(param8); value8.set(param8);
}, String.class, String.class, String.class, Boolean.class, Double.class, String.class, String.class, String.class); }, String.class, String.class, String.class, Boolean.class, Double.class, String.class, String.class, String.class);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"A\", \"B\", \"C\",true,12,\"D\",\"E\",\"F\"]}" + RECORD_SEPARATOR); mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[\"A\", \"B\", \"C\",true,12,\"D\",\"E\",\"F\"]}" + RECORD_SEPARATOR);
// Confirming that our handler was called and the correct message was passed in. // Confirming that our handler was called and the correct message was passed in.
assertEquals("A", value1.get()); assertEquals("A", value1.get());
@ -789,7 +789,7 @@ class HubConnectionTest {
value1.set(param1); value1.set(param1);
}, Custom.class); }, Custom.class);
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[{\"number\":1,\"str\":\"A\",\"bools\":[true,false]}]}" + RECORD_SEPARATOR); mockTransport.receiveMessage("{\"type\":1,\"target\":\"inc\",\"arguments\":[{\"number\":1,\"str\":\"A\",\"bools\":[true,false]}]}" + RECORD_SEPARATOR);
// Confirming that our handler was called and the correct message was passed in. // Confirming that our handler was called and the correct message was passed in.
@ -802,18 +802,19 @@ class HubConnectionTest {
} }
@Test @Test
public void receiveHandshakeResponseAndMessage() { public void receiveHandshakeResponseAndMessage() {
AtomicReference<Double> value = new AtomicReference<Double>(0.0); AtomicReference<Double> value = new AtomicReference<Double>(0.0);
MockTransport mockTransport = new MockTransport(false); MockTransport mockTransport = new MockTransport(false);
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com", mockTransport);
hubConnection.on("inc", () ->{ hubConnection.on("inc", () -> {
assertEquals(Double.valueOf(0), value.get()); assertEquals(Double.valueOf(0), value.get());
value.getAndUpdate((val) -> val + 1); value.getAndUpdate((val) -> val + 1);
}); });
// On start we're going to receive the handshake response and also an invocation in the same payload. // On start we're going to receive the handshake response and also an invocation in the same payload.
hubConnection.start(); hubConnection.start();
mockTransport.getStartTask().timeout(1, TimeUnit.SECONDS).blockingAwait();
String expectedSentMessage = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR; String expectedSentMessage = "{\"protocol\":\"json\",\"version\":1}" + RECORD_SEPARATOR;
assertEquals(expectedSentMessage, mockTransport.getSentMessages()[0]); assertEquals(expectedSentMessage, mockTransport.getSentMessages()[0]);
@ -827,7 +828,7 @@ class HubConnectionTest {
public void onClosedCallbackRunsWhenStopIsCalled() { public void onClosedCallbackRunsWhenStopIsCalled() {
AtomicReference<String> value1 = new AtomicReference<>(); AtomicReference<String> value1 = new AtomicReference<>();
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com"); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com");
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
hubConnection.onClosed((ex) -> { hubConnection.onClosed((ex) -> {
assertNull(value1.get()); assertNull(value1.get());
value1.set("Closed callback ran."); value1.set("Closed callback ran.");
@ -843,7 +844,7 @@ class HubConnectionTest {
AtomicReference<String> value1 = new AtomicReference<>(); AtomicReference<String> value1 = new AtomicReference<>();
AtomicReference<String> value2 = new AtomicReference<>(); AtomicReference<String> value2 = new AtomicReference<>();
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com"); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com");
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
hubConnection.onClosed((ex) -> { hubConnection.onClosed((ex) -> {
assertNull(value1.get()); assertNull(value1.get());
@ -871,7 +872,7 @@ class HubConnectionTest {
hubConnection.onClosed((ex) -> { hubConnection.onClosed((ex) -> {
assertEquals(ex.getMessage(), "There was an error"); assertEquals(ex.getMessage(), "There was an error");
}); });
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
@ -883,10 +884,10 @@ class HubConnectionTest {
@Test @Test
public void callingStartOnStartedHubConnectionNoOps() { public void callingStartOnStartedHubConnectionNoOps() {
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com"); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com");
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
hubConnection.stop(); hubConnection.stop();
@ -910,9 +911,8 @@ class HubConnectionTest {
assertTrue(false); assertTrue(false);
}, String.class); }, String.class);
Completable startFuture = hubConnection.start(); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
startFuture.blockingAwait(1000, TimeUnit.MILLISECONDS);
RuntimeException exception = assertThrows(RuntimeException.class, () -> mockTransport.receiveMessage("{\"type\":1,\"target\":\"Send\",\"arguments\":[]}" + RECORD_SEPARATOR)); RuntimeException exception = assertThrows(RuntimeException.class, () -> mockTransport.receiveMessage("{\"type\":1,\"target\":\"Send\",\"arguments\":[]}" + RECORD_SEPARATOR));
assertEquals("Invocation provides 0 argument(s) but target expects 1.", exception.getMessage()); assertEquals("Invocation provides 0 argument(s) but target expects 1.", exception.getMessage());
} }
@ -920,16 +920,15 @@ class HubConnectionTest {
@Test @Test
public void negotiateSentOnStart() { public void negotiateSentOnStart() {
TestHttpClient client = new TestHttpClient() TestHttpClient client = new TestHttpClient()
.on("POST", (req) -> CompletableFuture.completedFuture(new HttpResponse(404, "", ""))); .on("POST", (req) -> Single.just(new HttpResponse(404, "", "")));
HubConnection hubConnection = HubConnectionBuilder HubConnection hubConnection = HubConnectionBuilder
.create("http://example.com") .create("http://example.com")
.withHttpClient(client) .withHttpClient(client)
.build(); .build();
try { Exception exception = assertThrows(RuntimeException.class, () -> hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait());
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); assertEquals("Unexpected status code returned from negotiate: 404 .", exception.getMessage());
} catch(Exception ex) {}
List<HttpRequest> sentRequests = client.getSentRequests(); List<HttpRequest> sentRequests = client.getSentRequests();
assertEquals(1, sentRequests.size()); assertEquals(1, sentRequests.size());
@ -939,7 +938,7 @@ class HubConnectionTest {
@Test @Test
public void negotiateThatRedirectsForeverFailsAfter100Tries() { public void negotiateThatRedirectsForeverFailsAfter100Tries() {
TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate", TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate",
(req) -> CompletableFuture.completedFuture(new HttpResponse(200, "", "{\"url\":\"http://example.com\"}"))); (req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://example.com\"}")));
HubConnection hubConnection = HubConnectionBuilder HubConnection hubConnection = HubConnectionBuilder
.create("http://example.com") .create("http://example.com")
@ -947,14 +946,14 @@ class HubConnectionTest {
.build(); .build();
RuntimeException exception = assertThrows(RuntimeException.class, RuntimeException exception = assertThrows(RuntimeException.class,
() -> hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS)); () -> hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait());
assertEquals("Negotiate redirection limit exceeded.", exception.getCause().getCause().getMessage()); assertEquals("Negotiate redirection limit exceeded.", exception.getMessage());
} }
@Test @Test
public void afterSuccessfulNegotiateConnectsWithTransport() { public void afterSuccessfulNegotiateConnectsWithTransport() {
TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate", TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate",
(req) -> CompletableFuture.completedFuture(new HttpResponse(200, "", (req) -> Single.just(new HttpResponse(200, "",
"{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")));
@ -965,7 +964,7 @@ class HubConnectionTest {
.withHttpClient(client) .withHttpClient(client)
.build(); .build();
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
String[] sentMessages = transport.getSentMessages(); String[] sentMessages = transport.getSentMessages();
assertEquals(1, sentMessages.length); assertEquals(1, sentMessages.length);
@ -975,7 +974,7 @@ class HubConnectionTest {
@Test @Test
public void negotiateThatReturnsErrorThrowsFromStart() { public void negotiateThatReturnsErrorThrowsFromStart() {
TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate", TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate",
(req) -> CompletableFuture.completedFuture(new HttpResponse(200, "", "{\"error\":\"Test error.\"}"))); (req) -> Single.just(new HttpResponse(200, "", "{\"error\":\"Test error.\"}")));
MockTransport transport = new MockTransport(true); MockTransport transport = new MockTransport(true);
HubConnection hubConnection = HubConnectionBuilder HubConnection hubConnection = HubConnectionBuilder
@ -985,16 +984,16 @@ class HubConnectionTest {
.build(); .build();
RuntimeException exception = assertThrows(RuntimeException.class, RuntimeException exception = assertThrows(RuntimeException.class,
() -> hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS)); () -> hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait());
assertEquals("Test error.", exception.getCause().getCause().getMessage()); assertEquals("Test error.", exception.getMessage());
} }
@Test @Test
public void negotiateRedirectIsFollowed() { public void negotiateRedirectIsFollowed() {
TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate", TestHttpClient client = new TestHttpClient().on("POST", "http://example.com/negotiate",
(req) -> CompletableFuture.completedFuture(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\"}"))) (req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\"}")))
.on("POST", "http://testexample.com/negotiate", .on("POST", "http://testexample.com/negotiate",
(req) -> CompletableFuture.completedFuture(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\"" (req) -> Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")));
MockTransport transport = new MockTransport(true); MockTransport transport = new MockTransport(true);
@ -1004,7 +1003,7 @@ class HubConnectionTest {
.withHttpClient(client) .withHttpClient(client)
.build(); .build();
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
hubConnection.stop(); hubConnection.stop();
} }
@ -1016,8 +1015,7 @@ class HubConnectionTest {
.on("POST", "http://example.com/negotiate", .on("POST", "http://example.com/negotiate",
(req) -> { (req) -> {
token.set(req.getHeaders().get("Authorization")); token.set(req.getHeaders().get("Authorization"));
return CompletableFuture return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
.completedFuture(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"));
}); });
@ -1029,7 +1027,7 @@ class HubConnectionTest {
.withAccessTokenProvider(Single.just("secretToken")) .withAccessTokenProvider(Single.just("secretToken"))
.build(); .build();
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
hubConnection.stop(); hubConnection.stop();
assertEquals("Bearer secretToken", token.get()); assertEquals("Bearer secretToken", token.get());
@ -1039,11 +1037,10 @@ class HubConnectionTest {
public void accessTokenProviderIsOverriddenFromRedirectNegotiate() { public void accessTokenProviderIsOverriddenFromRedirectNegotiate() {
AtomicReference<String> token = new AtomicReference<>(); AtomicReference<String> token = new AtomicReference<>();
TestHttpClient client = new TestHttpClient() TestHttpClient client = new TestHttpClient()
.on("POST", "http://example.com/negotiate", (req) -> CompletableFuture.completedFuture(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\",\"accessToken\":\"newToken\"}"))) .on("POST", "http://example.com/negotiate", (req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\",\"accessToken\":\"newToken\"}")))
.on("POST", "http://testexample.com/negotiate", (req) -> { .on("POST", "http://testexample.com/negotiate", (req) -> {
token.set(req.getHeaders().get("Authorization")); token.set(req.getHeaders().get("Authorization"));
return CompletableFuture return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
.completedFuture(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"));
}); });
@ -1055,7 +1052,7 @@ class HubConnectionTest {
.withAccessTokenProvider(Single.just("secretToken")) .withAccessTokenProvider(Single.just("secretToken"))
.build(); .build();
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
assertEquals("http://testexample.com/?id=bVOiRPG8-6YiJ6d7ZcTOVQ", transport.getUrl()); assertEquals("http://testexample.com/?id=bVOiRPG8-6YiJ6d7ZcTOVQ", transport.getUrl());
hubConnection.stop(); hubConnection.stop();
@ -1067,14 +1064,14 @@ class HubConnectionTest {
HubConnection hubConnection = TestUtils.createHubConnection("http://example.com"); HubConnection hubConnection = TestUtils.createHubConnection("http://example.com");
hubConnection.setServerTimeout(1); hubConnection.setServerTimeout(1);
hubConnection.setTickRate(1); hubConnection.setTickRate(1);
CompletableFuture<Exception> closedFuture = new CompletableFuture<>(); SingleSubject<Exception> closedSubject = SingleSubject.create();
hubConnection.onClosed((e) -> { hubConnection.onClosed((e) -> {
closedFuture.complete(e); closedSubject.onSuccess(e);
}); });
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals("Server timeout elapsed without receiving a message from the server.", closedFuture.get(1000, TimeUnit.MILLISECONDS).getMessage()); assertEquals("Server timeout elapsed without receiving a message from the server.", closedSubject.timeout(1, TimeUnit.SECONDS).blockingGet().getMessage());
} }
@Test @Test
@ -1084,7 +1081,7 @@ class HubConnectionTest {
hubConnection.setKeepAliveInterval(1); hubConnection.setKeepAliveInterval(1);
hubConnection.setTickRate(1); hubConnection.setTickRate(1);
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
TimeUnit.MILLISECONDS.sleep(100); TimeUnit.MILLISECONDS.sleep(100);
hubConnection.stop(); hubConnection.stop();
@ -1103,8 +1100,7 @@ class HubConnectionTest {
.on("POST", "http://example.com/negotiate", .on("POST", "http://example.com/negotiate",
(req) -> { (req) -> {
header.set(req.getHeaders().get("ExampleHeader")); header.set(req.getHeaders().get("ExampleHeader"));
return CompletableFuture return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
.completedFuture(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"));
}); });
@ -1116,7 +1112,7 @@ class HubConnectionTest {
.withHeader("ExampleHeader", "ExampleValue") .withHeader("ExampleHeader", "ExampleValue")
.build(); .build();
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
hubConnection.stop(); hubConnection.stop();
assertEquals("ExampleValue", header.get()); assertEquals("ExampleValue", header.get());
@ -1129,8 +1125,7 @@ class HubConnectionTest {
.on("POST", "http://example.com/negotiate", .on("POST", "http://example.com/negotiate",
(req) -> { (req) -> {
header.set(req.getHeaders().get("ExampleHeader")); header.set(req.getHeaders().get("ExampleHeader"));
return CompletableFuture return Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
.completedFuture(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")); + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"));
}); });
@ -1143,7 +1138,7 @@ class HubConnectionTest {
.withHeader("ExampleHeader", "New Value") .withHeader("ExampleHeader", "New Value")
.build(); .build();
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
hubConnection.stop(); hubConnection.stop();
assertEquals("New Value", header.get()); assertEquals("New Value", header.get());
@ -1158,13 +1153,13 @@ class HubConnectionTest {
.shouldSkipNegotiate(true) .shouldSkipNegotiate(true)
.build(); .build();
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
hubConnection.stop().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.stop().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState());
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
hubConnection.stop().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.stop().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState());
} }
@ -1172,10 +1167,8 @@ class HubConnectionTest {
public void hubConnectionCanBeStartedAfterBeingStoppedAndRedirected() { public void hubConnectionCanBeStartedAfterBeingStoppedAndRedirected() {
MockTransport mockTransport = new MockTransport(); MockTransport mockTransport = new MockTransport();
TestHttpClient client = new TestHttpClient() TestHttpClient client = new TestHttpClient()
.on("POST", "http://example.com/negotiate", (req) -> CompletableFuture .on("POST", "http://example.com/negotiate", (req) -> Single.just(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\"}")))
.completedFuture(new HttpResponse(200, "", "{\"url\":\"http://testexample.com/\"}"))) .on("POST", "http://testexample.com/negotiate", (req) -> Single.just(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
.on("POST", "http://testexample.com/negotiate", (req) -> CompletableFuture
.completedFuture(new HttpResponse(200, "", "{\"connectionId\":\"bVOiRPG8-6YiJ6d7ZcTOVQ\",\""
+ "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}"))); + "availableTransports\":[{\"transport\":\"WebSockets\",\"transferFormats\":[\"Text\",\"Binary\"]}]}")));
HubConnection hubConnection = HubConnectionBuilder HubConnection hubConnection = HubConnectionBuilder
@ -1184,13 +1177,13 @@ class HubConnectionTest {
.withHttpClient(client) .withHttpClient(client)
.build(); .build();
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
hubConnection.stop().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.stop().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState());
hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.CONNECTED, hubConnection.getConnectionState());
hubConnection.stop().blockingAwait(1000, TimeUnit.MILLISECONDS); hubConnection.stop().timeout(1, TimeUnit.SECONDS).blockingAwait();
assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState()); assertEquals(HubConnectionState.DISCONNECTED, hubConnection.getConnectionState());
} }
@ -1198,8 +1191,7 @@ class HubConnectionTest {
public void non200FromNegotiateThrowsError() { public void non200FromNegotiateThrowsError() {
TestHttpClient client = new TestHttpClient() TestHttpClient client = new TestHttpClient()
.on("POST", "http://example.com/negotiate", .on("POST", "http://example.com/negotiate",
(req) -> CompletableFuture (req) -> Single.just(new HttpResponse(500, "Internal server error", "")));
.completedFuture(new HttpResponse(500, "Internal server error", "")));
MockTransport transport = new MockTransport(); MockTransport transport = new MockTransport();
HubConnection hubConnection = HubConnectionBuilder HubConnection hubConnection = HubConnectionBuilder
@ -1209,7 +1201,7 @@ class HubConnectionTest {
.build(); .build();
RuntimeException exception = assertThrows(RuntimeException.class, RuntimeException exception = assertThrows(RuntimeException.class,
() -> hubConnection.start().blockingAwait(1000, TimeUnit.MILLISECONDS)); () -> hubConnection.start().timeout(1, TimeUnit.SECONDS).blockingAwait());
assertEquals("Unexpected status code returned from negotiate: 500 Internal server error.", exception.getCause().getCause().getMessage()); assertEquals("Unexpected status code returned from negotiate: 500 Internal server error.", exception.getMessage());
} }
} }

View File

@ -4,9 +4,11 @@
package com.microsoft.signalr; package com.microsoft.signalr;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer; import java.util.function.Consumer;
import io.reactivex.Completable;
import io.reactivex.subjects.CompletableSubject;
class MockTransport implements Transport { class MockTransport implements Transport {
private OnReceiveCallBack onReceiveCallBack; private OnReceiveCallBack onReceiveCallBack;
private ArrayList<String> sentMessages = new ArrayList<>(); private ArrayList<String> sentMessages = new ArrayList<>();
@ -14,6 +16,8 @@ class MockTransport implements Transport {
private Consumer<String> onClose; private Consumer<String> onClose;
final private boolean ignorePings; final private boolean ignorePings;
final private boolean autoHandshake; final private boolean autoHandshake;
final private CompletableSubject startSubject = CompletableSubject.create();
final private CompletableSubject stopSubject = CompletableSubject.create();
private static final String RECORD_SEPARATOR = "\u001e"; private static final String RECORD_SEPARATOR = "\u001e";
@ -31,7 +35,7 @@ class MockTransport implements Transport {
} }
@Override @Override
public CompletableFuture<Void> start(String url) { public Completable start(String url) {
this.url = url; this.url = url;
if (autoHandshake) { if (autoHandshake) {
try { try {
@ -40,15 +44,16 @@ class MockTransport implements Transport {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
return CompletableFuture.completedFuture(null); startSubject.onComplete();
return startSubject;
} }
@Override @Override
public CompletableFuture<Void> send(String message) { public Completable send(String message) {
if (!(ignorePings && message.equals("{\"type\":6}" + RECORD_SEPARATOR))) { if (!(ignorePings && message.equals("{\"type\":6}" + RECORD_SEPARATOR))) {
sentMessages.add(message); sentMessages.add(message);
} }
return CompletableFuture.completedFuture(null); return Completable.complete();
} }
@Override @Override
@ -67,9 +72,10 @@ class MockTransport implements Transport {
} }
@Override @Override
public CompletableFuture<Void> stop() { public Completable stop() {
onClose.accept(null); onClose.accept(null);
return CompletableFuture.completedFuture(null); stopSubject.onComplete();
return stopSubject;
} }
public void stopWithError(String errorMessage) { public void stopWithError(String errorMessage) {
@ -87,4 +93,12 @@ class MockTransport implements Transport {
public String getUrl() { public String getUrl() {
return this.url; return this.url;
} }
public Completable getStartTask() {
return startSubject;
}
public Completable getStopTask() {
return stopSubject;
}
} }

View File

@ -6,24 +6,23 @@ package com.microsoft.signalr;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function; import java.util.function.Function;
import io.reactivex.Single;
class TestHttpClient extends HttpClient { class TestHttpClient extends HttpClient {
private Function<HttpRequest, CompletableFuture<HttpResponse>> handler; private Function<HttpRequest, Single<HttpResponse>> handler;
private List<HttpRequest> sentRequests; private List<HttpRequest> sentRequests;
public TestHttpClient() { public TestHttpClient() {
this.sentRequests = new ArrayList<>(); this.sentRequests = new ArrayList<>();
this.handler = (req) -> { this.handler = (req) -> {
CompletableFuture<HttpResponse> future = new CompletableFuture<>(); return Single.error(new RuntimeException(String.format("Request has no handler: %s %s", req.getMethod(), req.getUrl())));
future.completeExceptionally(new RuntimeException(String.format("Request has no handler: %s %s", req.getMethod(), req.getUrl())));
return future;
}; };
} }
@Override @Override
public CompletableFuture<HttpResponse> send(HttpRequest request) { public Single<HttpResponse> send(HttpRequest request) {
this.sentRequests.add(request); this.sentRequests.add(request);
return this.handler.apply(request); return this.handler.apply(request);
} }
@ -32,13 +31,13 @@ class TestHttpClient extends HttpClient {
return sentRequests; return sentRequests;
} }
public TestHttpClient on(Function<HttpRequest, CompletableFuture<HttpResponse>> handler) { public TestHttpClient on(Function<HttpRequest, Single<HttpResponse>> handler) {
this.handler = (req) -> handler.apply(req); this.handler = (req) -> handler.apply(req);
return this; return this;
} }
public TestHttpClient on(String method, Function<HttpRequest, CompletableFuture<HttpResponse>> handler) { public TestHttpClient on(String method, Function<HttpRequest, Single<HttpResponse>> handler) {
Function<HttpRequest, CompletableFuture<HttpResponse>> oldHandler = this.handler; Function<HttpRequest, Single<HttpResponse>> oldHandler = this.handler;
this.handler = (req) -> { this.handler = (req) -> {
if (req.getMethod().equals(method)) { if (req.getMethod().equals(method)) {
return handler.apply(req); return handler.apply(req);
@ -50,8 +49,8 @@ class TestHttpClient extends HttpClient {
return this; return this;
} }
public TestHttpClient on(String method, String url, Function<HttpRequest, CompletableFuture<HttpResponse>> handler) { public TestHttpClient on(String method, String url, Function<HttpRequest, Single<HttpResponse>> handler) {
Function<HttpRequest, CompletableFuture<HttpResponse>> oldHandler = this.handler; Function<HttpRequest, Single<HttpResponse>> oldHandler = this.handler;
this.handler = (req) -> { this.handler = (req) -> {
if (req.getMethod().equals(method) && req.getUrl().equals(url)) { if (req.getMethod().equals(method) && req.getUrl().equals(url)) {
return handler.apply(req); return handler.apply(req);

View File

@ -6,7 +6,6 @@ package com.microsoft.signalr;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import java.util.HashMap; import java.util.HashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -15,7 +14,7 @@ class WebSocketTransportTest {
@Test @Test
public void WebsocketThrowsIfItCantConnect() { public void WebsocketThrowsIfItCantConnect() {
Transport transport = new WebSocketTransport(new HashMap<>(), new DefaultHttpClient()); Transport transport = new WebSocketTransport(new HashMap<>(), new DefaultHttpClient());
ExecutionException exception = assertThrows(ExecutionException.class, () -> transport.start("http://www.example.com").get(1, TimeUnit.SECONDS)); RuntimeException exception = assertThrows(RuntimeException.class, () -> transport.start("http://www.example.com").blockingAwait(1, TimeUnit.SECONDS));
assertEquals("There was an error starting the Websockets transport.", exception.getCause().getMessage()); assertEquals("There was an error starting the Websockets transport.", exception.getMessage());
} }
} }