Back to Repositories

Testing RetryableRestTemplate Implementation in Apollo Config

This test suite validates the RetryableRestTemplate component in Apollo Portal, focusing on HTTP request retries, error handling, and authentication token management. It ensures reliable communication between Apollo Portal and Admin services across different environments.

Test Coverage Overview

The test suite provides comprehensive coverage of REST template functionality including:
  • Server availability and failover scenarios
  • HTTP method implementations (GET, POST, PUT, DELETE)
  • Authentication token handling across environments
  • Error handling for connection timeouts and server failures
  • Request retry logic and failure recovery

Implementation Analysis

The testing approach uses JUnit and Mockito frameworks to validate the RetryableRestTemplate implementation. It employs mocking to simulate network conditions and service responses, with particular attention to handling different types of exceptions and retry scenarios.

The tests verify both successful operations and error cases, ensuring proper handling of timeouts, connection failures, and authentication.

Technical Details

Key technical components include:
  • JUnit 4 test framework
  • Mockito for service mocking
  • Spring RestTemplate integration
  • HTTP request/response handling
  • Environment-specific configuration testing
  • Authentication token management

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Comprehensive mock setup for external dependencies
  • Thorough error case coverage
  • Clear test method naming and organization
  • Proper assertion usage for verification
  • Isolation of test scenarios
  • Effective use of JUnit and Mockito features

apolloconfig/apollo

apollo-portal/src/test/java/com/ctrip/framework/apollo/portal/RetryableRestTemplateTest.java

            
/*
 * Copyright 2024 Apollo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package com.ctrip.framework.apollo.portal;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.ctrip.framework.apollo.common.exception.ServiceException;
import com.ctrip.framework.apollo.core.dto.ServiceDTO;
import com.ctrip.framework.apollo.portal.component.AdminServiceAddressLocator;
import com.ctrip.framework.apollo.portal.component.RetryableRestTemplate;
import com.ctrip.framework.apollo.portal.component.config.PortalConfig;
import com.ctrip.framework.apollo.portal.environment.Env;
import com.ctrip.framework.apollo.portal.environment.PortalMetaDomainService;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.http.HttpHost;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.HttpHostConnectException;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;

public class RetryableRestTemplateTest extends AbstractUnitTest {

  @Mock
  private AdminServiceAddressLocator serviceAddressLocator;
  @Mock
  private RestTemplate restTemplate;
  @Mock
  private PortalMetaDomainService portalMetaDomainService;
  @Mock
  private PortalConfig portalConfig;
  @InjectMocks
  private RetryableRestTemplate retryableRestTemplate;

  private static final Gson GSON = new Gson();

  private String path = "app";
  private String serviceOne = "http://10.0.0.1";
  private String serviceTwo = "http://10.0.0.2";
  private String serviceThree = "http://10.0.0.3";
  private ResourceAccessException socketTimeoutException = new ResourceAccessException("");
  private ResourceAccessException httpHostConnectException = new ResourceAccessException("");
  private ResourceAccessException connectTimeoutException = new ResourceAccessException("");
  private Object request = new Object();
  private Object result = new Object();
  private Class<?> requestType = request.getClass();

  @Before
  public void init() {
    socketTimeoutException.initCause(new SocketTimeoutException());

    httpHostConnectException
        .initCause(new HttpHostConnectException(new ConnectTimeoutException(),
            new HttpHost(serviceOne, 80)));
    connectTimeoutException.initCause(new ConnectTimeoutException());
  }

  @Test(expected = ServiceException.class)
  public void testNoAdminServer() {

    when(serviceAddressLocator.getServiceList(any())).thenReturn(Collections.emptyList());

    retryableRestTemplate.get(Env.DEV, path, Object.class);
  }

  @Test(expected = ServiceException.class)
  public void testAllServerDown() {

    when(serviceAddressLocator.getServiceList(any()))
        .thenReturn(Arrays
            .asList(mockService(serviceOne), mockService(serviceTwo), mockService(serviceThree)));
    when(restTemplate
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(Object.class))).thenThrow(socketTimeoutException);
    when(restTemplate
        .exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(Object.class))).thenThrow(httpHostConnectException);
    when(restTemplate
        .exchange(eq(serviceThree + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(Object.class))).thenThrow(connectTimeoutException);

    retryableRestTemplate.get(Env.DEV, path, Object.class);

    verify(restTemplate, times(1))
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(Object.class));
    verify(restTemplate, times(1))
        .exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(Object.class));
    verify(restTemplate, times(1))
        .exchange(eq(serviceThree + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(Object.class));
  }

  @Test
  public void testOneServerDown() {
    ResponseEntity someEntity = mock(ResponseEntity.class);
    when(someEntity.getBody()).thenReturn(result);

    when(serviceAddressLocator.getServiceList(any()))
        .thenReturn(Arrays
            .asList(mockService(serviceOne), mockService(serviceTwo), mockService(serviceThree)));
    when(restTemplate
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(Object.class))).thenThrow(socketTimeoutException);
    when(restTemplate
        .exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(Object.class))).thenReturn(someEntity);
    when(restTemplate
        .exchange(eq(serviceThree + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(Object.class))).thenThrow(connectTimeoutException);

    Object actualResult = retryableRestTemplate.get(Env.DEV, path, Object.class);

    verify(restTemplate, times(1))
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(Object.class));
    verify(restTemplate, times(1))
        .exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(Object.class));
    verify(restTemplate, never())
        .exchange(eq(serviceThree + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(Object.class));
    assertEquals(result, actualResult);
  }

  @Test
  public void testPostSocketTimeoutNotRetry() {
    ResponseEntity someEntity = mock(ResponseEntity.class);
    when(someEntity.getBody()).thenReturn(result);

    when(serviceAddressLocator.getServiceList(any()))
        .thenReturn(Arrays
            .asList(mockService(serviceOne), mockService(serviceTwo), mockService(serviceThree)));
    when(restTemplate
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class),
            eq(Object.class))).thenThrow(socketTimeoutException);
    when(restTemplate
        .exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class),
            eq(Object.class))).thenReturn(someEntity);

    Throwable exception = null;
    Object actualResult = null;
    try {
      actualResult = retryableRestTemplate.post(Env.DEV, path, request, Object.class);
    } catch (Throwable ex) {
      exception = ex;
    }

    assertNull(actualResult);
    assertSame(socketTimeoutException, exception);
    verify(restTemplate, times(1))
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class),
            eq(Object.class));
    verify(restTemplate, never())
        .exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class),
            eq(Object.class));
  }

  @Test
  public void testDelete() {
    ResponseEntity someEntity = mock(ResponseEntity.class);

    when(serviceAddressLocator.getServiceList(any()))
        .thenReturn(Arrays
            .asList(mockService(serviceOne), mockService(serviceTwo), mockService(serviceThree)));
    when(restTemplate
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.DELETE), any(HttpEntity.class),
            (Class<Object>) isNull())).thenReturn(someEntity);

    retryableRestTemplate.delete(Env.DEV, path);

    verify(restTemplate)
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.DELETE), any(HttpEntity.class),
            (Class<Object>) isNull());
  }

  @Test
  public void testPut() {
    ResponseEntity someEntity = mock(ResponseEntity.class);

    when(serviceAddressLocator.getServiceList(any()))
        .thenReturn(Arrays
            .asList(mockService(serviceOne), mockService(serviceTwo), mockService(serviceThree)));
    when(restTemplate
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.PUT), any(HttpEntity.class),
            (Class<Object>) isNull())).thenReturn(someEntity);

    retryableRestTemplate.put(Env.DEV, path, request);

    ArgumentCaptor<HttpEntity> argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class);
    verify(restTemplate)
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.PUT), argumentCaptor.capture(),
            (Class<Object>) isNull());

    assertEquals(request, argumentCaptor.getValue().getBody());
  }

  @Test
  public void testPostObjectWithNoAccessToken() {
    Env someEnv = Env.DEV;
    ResponseEntity someEntity = mock(ResponseEntity.class);

    when(serviceAddressLocator.getServiceList(someEnv))
        .thenReturn(Collections.singletonList(mockService(serviceOne)));
    when(restTemplate
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class),
            eq(requestType))).thenReturn(someEntity);
    when(someEntity.getBody()).thenReturn(result);

    Object actualResult = retryableRestTemplate.post(someEnv, path, request, requestType);

    assertEquals(result, actualResult);

    ArgumentCaptor<HttpEntity> argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class);
    verify(restTemplate, times(1))
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), argumentCaptor.capture(),
            eq(requestType));

    HttpEntity entity = argumentCaptor.getValue();
    HttpHeaders headers = entity.getHeaders();

    assertSame(request, entity.getBody());
    assertTrue(headers.isEmpty());
  }

  @Test
  public void testPostObjectWithAccessToken() {
    Env someEnv = Env.DEV;
    String someToken = "someToken";
    ResponseEntity someEntity = mock(ResponseEntity.class);

    when(portalConfig.getAdminServiceAccessTokens())
        .thenReturn(mockAdminServiceTokens(someEnv, someToken));
    when(serviceAddressLocator.getServiceList(someEnv))
        .thenReturn(Collections.singletonList(mockService(serviceOne)));
    when(restTemplate
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class),
            eq(requestType))).thenReturn(someEntity);
    when(someEntity.getBody()).thenReturn(result);

    Object actualResult = retryableRestTemplate.post(someEnv, path, request, requestType);

    assertEquals(result, actualResult);

    ArgumentCaptor<HttpEntity> argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class);
    verify(restTemplate, times(1))
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), argumentCaptor.capture(),
            eq(requestType));

    HttpEntity entity = argumentCaptor.getValue();
    HttpHeaders headers = entity.getHeaders();
    List<String> headerValue = headers.get(HttpHeaders.AUTHORIZATION);

    assertSame(request, entity.getBody());
    assertEquals(1, headers.size());
    assertEquals(1, headerValue.size());
    assertEquals(someToken, headerValue.get(0));
  }

  @Test
  public void testPostObjectWithNoAccessTokenForEnv() {
    Env someEnv = Env.DEV;
    Env anotherEnv = Env.PRO;
    String someToken = "someToken";
    ResponseEntity someEntity = mock(ResponseEntity.class);

    when(portalConfig.getAdminServiceAccessTokens())
        .thenReturn(mockAdminServiceTokens(someEnv, someToken));
    when(serviceAddressLocator.getServiceList(someEnv))
        .thenReturn(Collections.singletonList(mockService(serviceOne)));
    when(serviceAddressLocator.getServiceList(anotherEnv))
        .thenReturn(Collections.singletonList(mockService(serviceTwo)));
    when(restTemplate
        .exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class),
            eq(requestType))).thenReturn(someEntity);
    when(someEntity.getBody()).thenReturn(result);

    Object actualResult = retryableRestTemplate.post(anotherEnv, path, request, requestType);

    assertEquals(result, actualResult);

    ArgumentCaptor<HttpEntity> argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class);
    verify(restTemplate, times(1))
        .exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.POST), argumentCaptor.capture(),
            eq(requestType));

    HttpEntity entity = argumentCaptor.getValue();
    HttpHeaders headers = entity.getHeaders();

    assertSame(request, entity.getBody());
    assertTrue(headers.isEmpty());
  }

  @Test
  public void testPostEntityWithNoAccessToken() {
    Env someEnv = Env.DEV;
    String originalHeader = "someHeader";
    String originalValue = "someValue";
    HttpHeaders originalHeaders = new HttpHeaders();
    originalHeaders.add(originalHeader, originalValue);
    HttpEntity<Object> requestEntity = new HttpEntity<>(request, originalHeaders);
    ResponseEntity someEntity = mock(ResponseEntity.class);

    when(serviceAddressLocator.getServiceList(someEnv))
        .thenReturn(Collections.singletonList(mockService(serviceOne)));
    when(restTemplate
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class),
            eq(requestType))).thenReturn(someEntity);
    when(someEntity.getBody()).thenReturn(result);

    Object actualResult = retryableRestTemplate.post(someEnv, path, requestEntity, requestType);

    assertEquals(result, actualResult);

    ArgumentCaptor<HttpEntity> argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class);
    verify(restTemplate, times(1))
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), argumentCaptor.capture(),
            eq(requestType));

    HttpEntity entity = argumentCaptor.getValue();

    assertSame(requestEntity, entity);
    assertSame(request, entity.getBody());
    assertEquals(originalHeaders, entity.getHeaders());
  }

  @Test
  public void testPostEntityWithAccessToken() {
    Env someEnv = Env.DEV;
    String someToken = "someToken";
    String originalHeader = "someHeader";
    String originalValue = "someValue";
    HttpHeaders originalHeaders = new HttpHeaders();
    originalHeaders.add(originalHeader, originalValue);
    HttpEntity<Object> requestEntity = new HttpEntity<>(request, originalHeaders);
    ResponseEntity someEntity = mock(ResponseEntity.class);

    when(portalConfig.getAdminServiceAccessTokens())
        .thenReturn(mockAdminServiceTokens(someEnv, someToken));
    when(serviceAddressLocator.getServiceList(someEnv))
        .thenReturn(Collections.singletonList(mockService(serviceOne)));
    when(restTemplate
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), any(HttpEntity.class),
            eq(requestType))).thenReturn(someEntity);
    when(someEntity.getBody()).thenReturn(result);

    Object actualResult = retryableRestTemplate.post(someEnv, path, requestEntity, requestType);

    assertEquals(result, actualResult);

    ArgumentCaptor<HttpEntity> argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class);
    verify(restTemplate, times(1))
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.POST), argumentCaptor.capture(),
            eq(requestType));

    HttpEntity entity = argumentCaptor.getValue();
    HttpHeaders headers = entity.getHeaders();

    assertSame(request, entity.getBody());
    assertEquals(2, headers.size());
    assertEquals(originalValue, headers.get(originalHeader).get(0));
    assertEquals(someToken, headers.get(HttpHeaders.AUTHORIZATION).get(0));
  }

  @Test
  public void testGetEntityWithNoAccessToken() {
    Env someEnv = Env.DEV;
    ParameterizedTypeReference requestType = mock(ParameterizedTypeReference.class);
    ResponseEntity someEntity = mock(ResponseEntity.class);

    when(serviceAddressLocator.getServiceList(someEnv))
        .thenReturn(Collections.singletonList(mockService(serviceOne)));
    when(restTemplate
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(requestType))).thenReturn(someEntity);

    ResponseEntity actualResult = retryableRestTemplate.get(someEnv, path, requestType);

    assertEquals(someEntity, actualResult);

    ArgumentCaptor<HttpEntity> argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class);
    verify(restTemplate, times(1))
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), argumentCaptor.capture(),
            eq(requestType));

    HttpHeaders headers = argumentCaptor.getValue().getHeaders();

    assertTrue(headers.isEmpty());
  }

  @Test
  public void testGetEntityWithAccessToken() {
    Env someEnv = Env.DEV;
    String someToken = "someToken";
    ParameterizedTypeReference requestType = mock(ParameterizedTypeReference.class);
    ResponseEntity someEntity = mock(ResponseEntity.class);

    when(portalConfig.getAdminServiceAccessTokens())
        .thenReturn(mockAdminServiceTokens(someEnv, someToken));
    when(serviceAddressLocator.getServiceList(someEnv))
        .thenReturn(Collections.singletonList(mockService(serviceOne)));
    when(restTemplate
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(requestType))).thenReturn(someEntity);

    ResponseEntity actualResult = retryableRestTemplate.get(someEnv, path, requestType);

    assertEquals(someEntity, actualResult);

    ArgumentCaptor<HttpEntity> argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class);
    verify(restTemplate, times(1))
        .exchange(eq(serviceOne + "/" + path), eq(HttpMethod.GET), argumentCaptor.capture(),
            eq(requestType));

    HttpHeaders headers = argumentCaptor.getValue().getHeaders();
    List<String> headerValue = headers.get(HttpHeaders.AUTHORIZATION);

    assertEquals(1, headers.size());
    assertEquals(1, headerValue.size());
    assertEquals(someToken, headerValue.get(0));
  }

  @Test
  public void testGetEntityWithNoAccessTokenForEnv() {
    Env someEnv = Env.DEV;
    Env anotherEnv = Env.PRO;
    String someToken = "someToken";
    ParameterizedTypeReference requestType = mock(ParameterizedTypeReference.class);
    ResponseEntity someEntity = mock(ResponseEntity.class);

    when(portalConfig.getAdminServiceAccessTokens())
        .thenReturn(mockAdminServiceTokens(someEnv, someToken));
    when(serviceAddressLocator.getServiceList(someEnv))
        .thenReturn(Collections.singletonList(mockService(serviceOne)));
    when(serviceAddressLocator.getServiceList(anotherEnv))
        .thenReturn(Collections.singletonList(mockService(serviceTwo)));
    when(restTemplate
        .exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.GET), any(HttpEntity.class),
            eq(requestType))).thenReturn(someEntity);

    ResponseEntity actualResult = retryableRestTemplate.get(anotherEnv, path, requestType);

    assertEquals(someEntity, actualResult);

    ArgumentCaptor<HttpEntity> argumentCaptor = ArgumentCaptor.forClass(HttpEntity.class);
    verify(restTemplate, times(1))
        .exchange(eq(serviceTwo + "/" + path), eq(HttpMethod.GET), argumentCaptor.capture(),
            eq(requestType));

    HttpHeaders headers = argumentCaptor.getValue().getHeaders();

    assertTrue(headers.isEmpty());
  }

  private String mockAdminServiceTokens(Env env, String token) {
    Map<String, String> tokenMap = Maps.newHashMap();
    tokenMap.put(env.getName(), token);

    return GSON.toJson(tokenMap);
  }

  private ServiceDTO mockService(String homeUrl) {
    ServiceDTO serviceDTO = new ServiceDTO();
    serviceDTO.setHomepageUrl(homeUrl);
    return serviceDTO;
  }

}