/**
* Copyright 2013 Twitter, 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 com.twitter.hbc.httpclient;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.twitter.hbc.BasicReconnectionManager;
import com.twitter.hbc.RateTracker;
import com.twitter.hbc.ReconnectionManager;
import com.twitter.hbc.core.HttpConstants;
import com.twitter.hbc.core.HttpHosts;
import com.twitter.hbc.core.endpoint.RawEndpoint;
import com.twitter.hbc.core.event.EventType;
import com.twitter.hbc.core.processor.HosebirdMessageProcessor;
import com.twitter.hbc.httpclient.auth.Authentication;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.conn.ClientConnectionManager;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
public class BasicClientTest {
private HttpClient mockClient;
private HttpResponse mockResponse;
private HttpEntity mockHttpEntity;
private StatusLine mockStatusLine;
private ReconnectionManager mockReconnectionManager;
private HosebirdMessageProcessor mockProcessor;
private Authentication mockAuth;
private RateTracker mockRateTracker;
private InputStream mockInputStream;
private ClientConnectionManager mockConnectionManager;
private final ExecutorService executorService;
public BasicClientTest() {
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setDaemon(true)
.setNameFormat("hosebird-client-unit-test-%d")
.build();
executorService = Executors.newSingleThreadExecutor(threadFactory);
}
@Before
public void setup() throws Exception {
mockClient = mock(HttpClient.class);
mockResponse = mock(HttpResponse.class);
mockStatusLine = mock(StatusLine.class);
mockReconnectionManager = mock(BasicReconnectionManager.class);
mockConnectionManager = mock(ClientConnectionManager.class);
mockRateTracker = mock(RateTracker.class);
mockInputStream = mock(InputStream.class);
mockAuth = mock(Authentication.class);
mockProcessor = mock(HosebirdMessageProcessor.class);
mockHttpEntity = mock(HttpEntity.class);
// set up required mocks to mock out all of the clientbase stuff
when(mockClient.execute(any(HttpUriRequest.class)))
.thenReturn(mockResponse);
when(mockClient.getConnectionManager())
.thenReturn(mockConnectionManager);
when(mockResponse.getStatusLine())
.thenReturn(mockStatusLine);
when(mockResponse.getEntity())
.thenReturn(mockHttpEntity);
when(mockHttpEntity.getContent())
.thenReturn(mockInputStream);
when(mockStatusLine.getReasonPhrase())
.thenReturn("reason");
}
// These tests are going to get a little hairy in terms of mocking, but doable.
// Some of the functionality is already tested in ClientBaseTest, but this tests
// the overall flow. Worth it?
@Test
public void testIOExceptionDuringProcessing() throws Exception {
ClientBase clientBase = new ClientBase("name",
mockClient, new HttpHosts("http://hi"), new RawEndpoint("/endpoint", HttpConstants.HTTP_GET), mockAuth,
mockProcessor, mockReconnectionManager, mockRateTracker
);
BasicClient client = new BasicClient(clientBase, executorService);
final CountDownLatch latch = new CountDownLatch(1);
when(mockStatusLine.getStatusCode())
.thenReturn(200);
doNothing().when(mockProcessor).setup(any(InputStream.class));
doThrow(new IOException()).
doThrow(new IOException()).
doThrow(new IOException()).
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
latch.countDown();
return null;
}
}).when(mockProcessor).process();
client.connect();
latch.await();
assertFalse(clientBase.isDone());
verify(mockProcessor, times(4)).setup(any(InputStream.class));
// throw 3 exceptions, 4th one keeps going
verify(mockProcessor, atLeast(4)).process();
client.stop();
verify(mockConnectionManager, atLeastOnce()).shutdown();
assertTrue(client.isDone());
assertEquals(EventType.STOPPED_BY_USER, clientBase.getExitEvent().getEventType());
}
@Test
public void testInterruptedExceptionDuringProcessing() throws Exception {
ClientBase clientBase = new ClientBase("name",
mockClient, new HttpHosts("http://hi"), new RawEndpoint("/endpoint", HttpConstants.HTTP_GET), mockAuth,
mockProcessor, mockReconnectionManager, mockRateTracker
);
when(mockStatusLine.getStatusCode())
.thenReturn(200);
doThrow(new InterruptedException()).when(mockProcessor).process();
when(mockClient.getConnectionManager())
.thenReturn(mockConnectionManager);
BasicClient client = new BasicClient(clientBase, executorService);
assertFalse(clientBase.isDone());
client.connect();
assertTrue(client.waitForFinish(100));
assertTrue(client.isDone());
verify(mockProcessor).setup(any(InputStream.class));
verify(mockConnectionManager, atLeastOnce()).shutdown();
assertEquals(EventType.STOPPED_BY_ERROR, client.getExitEvent().getEventType());
assertTrue(client.getExitEvent().getUnderlyingException() instanceof InterruptedException);
}
@Test
public void testConnectionRetries() throws Exception {
HttpHosts mockHttpHosts = mock(HttpHosts.class);
ClientBase clientBase = new ClientBase("name",
mockClient, mockHttpHosts, new RawEndpoint("/endpoint", HttpConstants.HTTP_GET), mockAuth,
mockProcessor, mockReconnectionManager, mockRateTracker
);
BasicClient client = new BasicClient(clientBase, executorService);
final CountDownLatch latch = new CountDownLatch(1);
when(mockHttpHosts.nextHost())
.thenReturn("http://somehost.com");
when(mockClient.execute(any(HttpUriRequest.class)))
.thenReturn(mockResponse)
.thenReturn(mockResponse)
.thenThrow(new IOException())
.thenReturn(mockResponse);
when(mockStatusLine.getStatusCode())
.thenReturn(HttpConstants.Codes.UNAUTHORIZED)
.thenReturn(HttpConstants.Codes.SERVICE_UNAVAILABLE)
.thenReturn(HttpConstants.Codes.SUCCESS);
// turn off the client when we start processing
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
latch.countDown();
return null;
}
}).when(mockProcessor).process();
// for 401 Unauthorized
when(mockReconnectionManager.shouldReconnectOn400s()).thenReturn(true);
/** for shutdown **/
when(mockClient.getConnectionManager())
.thenReturn(mockConnectionManager);
assertFalse(clientBase.isDone());
client.connect();
latch.await();
client.stop();
assertTrue(client.isDone());
// exponential backoff twice: once for 401 once for 503
verify(mockReconnectionManager, times(2)).handleExponentialBackoff();
// for thrown IOException
verify(mockReconnectionManager).handleLinearBackoff();
// for successful connection
verify(mockReconnectionManager).resetCounts();
// finally start setting up processor/processing for the last attempt that goes through
verify(mockProcessor, atLeastOnce()).setup(any(InputStream.class));
verify(mockProcessor, atLeastOnce()).process();
assertEquals(EventType.STOPPED_BY_USER, clientBase.getExitEvent().getEventType());
verify(mockConnectionManager, atLeastOnce()).shutdown();
}
}