/*
* Copyright 2015 Netflix, Inc.
*
* 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 io.reactivex.netty.protocol.http.ws.client;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import io.reactivex.netty.channel.Connection;
import io.reactivex.netty.protocol.http.ws.WebSocketConnection;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExternalResource;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import rx.Observable;
import rx.Observable.OnSubscribe;
import rx.Subscriber;
import rx.functions.Func1;
import rx.observers.TestSubscriber;
import rx.subjects.Subject;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
public class OperatorCacheSingleWebsocketConnectionTest {
@Rule
public final OpRule opRule = new OpRule();
@Test(timeout = 60000)
public void testLifecycleNeverEnds() throws Exception {
opRule.subscribeAndAssertValues(opRule.getSourceWithCache().repeat(2), 1);
}
@Test(timeout = 60000)
public void testLifecycleCompletesImmediately() throws Exception {
opRule.subscribeAndAssertValues(opRule.getSourceWithCache()
.map(new Func1<WebSocketConnection, WebSocketConnection>() {
@Override
public WebSocketConnection call(WebSocketConnection c) {
opRule.terminateFirstConnection(null);
return c;
}
}).repeat(2),
2);// Since the cached item is immediately invalid, two items will be emitted from source.
}
@Test(timeout = 60000)
public void testLifecycleErrorsImmediately() throws Exception {
opRule.subscribeAndAssertValues(opRule.getSourceWithCache()
.map(new Func1<WebSocketConnection, WebSocketConnection>() {
@Override
public WebSocketConnection call(WebSocketConnection c) {
opRule.terminateFirstConnection(new IllegalStateException());
return c;
}
}).repeat(2),
2);// Since the cached item is immediately invalid, two items will be emitted from source.
}
@Test(timeout = 60000)
public void testSourceEmitsNoItems() throws Exception {
TestSubscriber<WebSocketConnection> subscriber = new TestSubscriber<>();
Observable.<Observable<Observable<WebSocketConnection>>>empty()
.lift(new OperatorCacheSingleWebsocketConnection())
.subscribe(subscriber);
subscriber.awaitTerminalEvent();
subscriber.assertError(IllegalStateException.class);
}
@Test(timeout = 60000)
public void testSourceEmitsError() throws Exception {
TestSubscriber<WebSocketConnection> subscriber = new TestSubscriber<>();
Observable.<Observable<WebSocketConnection>>error(new NullPointerException())
.nest()
.lift(new OperatorCacheSingleWebsocketConnection())
.subscribe(subscriber);
subscriber.awaitTerminalEvent();
subscriber.assertError(NullPointerException.class);
}
@Test(timeout = 60000)
public void testUnsubscribeFromLifecycle() throws Exception {
opRule.subscribeAndAssertValues(opRule.getSourceWithCache(), 1);
LifecycleSubject lifecycleSubject = opRule.lifecycles.poll();
assertThat("No subscribers to lifecycle.", lifecycleSubject.subscribers, hasSize(1));
assertThat("Lifecycle subscriber not unsubscribed.", lifecycleSubject.subscribers.poll().isUnsubscribed(),
is(true));
}
private static class OpRule extends ExternalResource {
private Observable<Observable<Observable<WebSocketConnection>>> source;
private ConcurrentLinkedQueue<LifecycleSubject> lifecycles;
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
lifecycles = new ConcurrentLinkedQueue<>();
source = Observable.create(new OnSubscribe<Observable<WebSocketConnection>>() {
@Override
public void call(Subscriber<? super Observable<WebSocketConnection>> s) {
LifecycleSubject l = new LifecycleSubject(new ConcurrentLinkedQueue<Subscriber<? super Void>>());
lifecycles.add(l);
WebSocketConnection conn = newConnection(l);
/*Subscriptions to the emitted Observable will always give the same connection but
subscription to the source (this Observable) creates a new connection. This simulates
how the actual websocket connection get works.*/
s.onNext(Observable.just(conn));
s.onCompleted();
}
}).nest();
base.evaluate();
}
};
}
public boolean terminateFirstConnection(Throwable error) {
LifecycleSubject poll = lifecycles.poll();
if (null != poll) {
if (null == error) {
poll.onCompleted();
} else {
poll.onError(error);
}
return true;
}
return false;
}
public Observable<WebSocketConnection> getSourceWithCache() {
return source.lift(new OperatorCacheSingleWebsocketConnection());
}
public void subscribeAndAssertValues(Observable<WebSocketConnection> source, int distinctItemsCount) {
TestSubscriber<WebSocketConnection> subscriber = new TestSubscriber<>();
source.subscribe(subscriber);
subscriber.awaitTerminalEvent();
subscriber.assertNoErrors();
List<WebSocketConnection> onNextEvents = subscriber.getOnNextEvents();
Set<WebSocketConnection> distinctConns = new HashSet<>(onNextEvents);
assertThat("Unexpected number of distinct connections.", distinctConns, hasSize(distinctItemsCount));
}
private WebSocketConnection newConnection(final Observable<Void> lifecycle) {
@SuppressWarnings("unchecked")
Connection<WebSocketFrame, WebSocketFrame> mock = Mockito.mock(Connection.class);
Mockito.when(mock.closeListener()).thenAnswer(new Answer<Observable<Void>>() {
@Override
public Observable<Void> answer(InvocationOnMock invocation) throws Throwable {
return lifecycle;
}
});
return new WebSocketConnection(mock);
}
}
private static class LifecycleSubject extends Subject<Void, Void> {
private final ConcurrentLinkedQueue<Subscriber<? super Void>> subscribers;
protected LifecycleSubject(final ConcurrentLinkedQueue<Subscriber<? super Void>> subscribers) {
super(new OnSubscribe<Void>() {
@Override
public void call(Subscriber<? super Void> subscriber) {
subscribers.add(subscriber);
}
});
this.subscribers = subscribers;
}
@Override
public boolean hasObservers() {
return !subscribers.isEmpty();
}
@Override
public void onCompleted() {
for (Subscriber<? super Void> subscriber : subscribers) {
subscriber.onCompleted();
}
}
@Override
public void onError(Throwable e) {
for (Subscriber<? super Void> subscriber : subscribers) {
subscriber.onError(e);
}
}
@Override
public void onNext(Void aVoid) {
// No op ...
}
}
}