Mostrando entradas con la etiqueta HTTPClient. Mostrar todas las entradas
Mostrando entradas con la etiqueta HTTPClient. Mostrar todas las entradas

lunes, 13 de febrero de 2023

Wiremock y JUnit5

Hoy vamos a ver un sencillo post sobre cómo configurar Wiremock con JUnit 5. Algo sencillo pero que cambia respecto a cómo era con JUnit 4 rules. Para nuestro ejemplo utilizaremos las siguientes versiones:

  • junit-jupiter-api:5.9.2
  • wiremock-jre8:2.35.0

Para empezar, si queremos utilizar Wiremock de forma muy básica, simplemente nos bastará con la anotación @WireMockTest. La cual nos permitirá modificar lo siguiente:

  • httpPort: Para indicar en que puerto podremos hacer llamadas HTTP.
  • httpsEnabled y httpsPort. Para indicar que queremos hacer llamadas HTTPS y en que puerto. 
  • proxyMode. Para emular un nombre de dominio distinto a localhost. En dicho caso y usando HTTPClient deberemos usar el método useSystemProperties a la hora de crear el cliente. 
A continuación podemos ver un ejemplo de un invocación a un nombre de dominio distinto a localhost y HTTPS. Para este ultimo ya no necesitaremos crear un certificado autoafirmado como lo hacía antiguamente. 

@Log4j2
@WireMockTest(httpsEnabled = true, httpsPort = 9090, proxyMode = true)
public class WiremockBasicTest {
  private static final String BEARER_TOKEN = "Bearer 77d9b8f0-fafe-3778-addf-2755bdc53c88";
  private static final String JSON_CONTENT = "{\"hellow\":\"world\"}";

  @Test
  public void doGetAndGetResponse_proxyMode() throws Exception{
    String sEndpoint = "https://mydomain.com:9090/sample";
    Map<String, String> headers = new HashMap<>();
    headers.put(HttpHeaders.AUTHORIZATION, BEARER_TOKEN);
    stubFor(get("/sample").withHeader(HttpHeaders.AUTHORIZATION, WireMock.equalTo(BEARER_TOKEN))
        .withHost(WireMock.equalTo("mydomain.com"))
        .willReturn(aResponse().withBody(JSON_CONTENT).withStatus(200)));

    String body = null;
    HttpGet get = new HttpGet(sEndpoint);
    get.setHeaders(headers.entrySet().stream().map(entry -> new BasicHeader(entry.getKey(), entry.getValue())).toArray(Header[]::new));
    try (CloseableHttpClient httpClient = createAcceptSelfSignedCertificateClient(); CloseableHttpResponse response = httpClient.execute(get)) {
        body = EntityUtils.toString(response.getEntity(), Charset.defaultCharset());
    }
    assertThat(body, equalTo(JSON_CONTENT));
  }
}

Y aunque con muy poco ya podemos hacer mucho. Puede que haya casos en los que necesitemos un poco más de configuración. Realizar las invocaciones como hacíamos antes con los JUnit 4 rules y tener acceso al método wireMockConfig

Esto lo podremos hacer a través de las extensiones de JUnit5. Pero la instancia que creemos será la misma que debemos utilizar para crear los distintos stub. Lo beneficioso de este enfoque, es que además nos permite crear distintas instancias de la extensión y usar ambas. 

public class WiremockComplexTest {
  private static final String BEARER_TOKEN = "Bearer 77d9b8f0-fafe-3778-addf-2755bdc53c88";
  private static final String JSON_CONTENT = "{\"hellow\":\"world\"}";

  @RegisterExtension
  static WireMockExtension wme = WireMockExtension.newInstance()
      .options(wireMockConfig().httpsPort(9090).port(8085)
      .notifier(new ConsoleNotifier(true))).proxyMode(true).build();

  @Test
  public void doGetAndGetResponse_proxyMode() throws Exception {
    String sEndpoint = "https://mydomain.com:9090/sample";
    Map<String, String> headers = new HashMap<>();
    headers.put(HttpHeaders.AUTHORIZATION, BEARER_TOKEN);
    wme.stubFor(get("/sample").withHeader(HttpHeaders.AUTHORIZATION, WireMock.equalTo(BEARER_TOKEN)).withHost(WireMock.equalTo("mydomain.com"))
        .willReturn(aResponse().withBody(JSON_CONTENT).withStatus(200)));

    String body = null;
    HttpGet get = new HttpGet(sEndpoint);
    get.setHeaders(headers.entrySet().stream().map(entry -> new BasicHeader(entry.getKey(), entry.getValue())).toArray(Header[]::new));
    try (CloseableHttpClient httpClient = createAcceptSelfSignedCertificateClient(); CloseableHttpResponse response = httpClient.execute(get)) {
      body = EntityUtils.toString(response.getEntity(), Charset.defaultCharset());
    }
    assertThat(body, equalTo(JSON_CONTENT));
  }
}

Por último, si este enfoque es el ideal para tus pruebas. Pero no quieres estar indicando la instancia en todos los métodos de WireMock. Podemos indicar que la instancia la cree de forma estática y ya si que sería de la misma forma que cuando usábamos los JUnit 4 rules. Para ello solo tendríamos que utilizar el método configureStaticDsl(true)

@RegisterExtension
static WireMockExtension wme = WireMockExtension.newInstance()
    .options(wireMockConfig().httpsPort(9090).port(8085)
    .notifier(new ConsoleNotifier(true)))
    .configureStaticDsl(true).proxyMode(true).build();

Espero que este post haya sido útil y os ayude en la actualización de vuestro software de pruebas. 

miércoles, 18 de enero de 2023

Http Client 5: Manual básico

Ya hemos hecho otros posts sobre esta gran librería, sobre configuración y uso de la interfaz Fluent. Hoy veremos un manual básico sobre la nueva versión, HTTP Client 5 en su versión clásica. Y cuatro sencillos ejemplos para su uso más común. 

Veremos el ejemplo más básico y completo, con una llamada HTTP Post. En 5 sencillos pasos, podemos invocar el método y obtener su respuesta. Incluso en métodos que no necesitan enviar datos, como GET o DELETE se puede hacer incluso en menos pasos. 

public static String post(final String url, final String jsonBody) {
  // 1. Create HTTP Method
  HttpPost httpPost = new HttpPost(url);
  // 2. Set payload and content-type
  httpPost.setEntity(new StringEntity(jsonBody, ContentType.APPLICATION_JSON));
  String result = null;
  // 3. Create HTTP client
  try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
    // 4. Execute the method through the HTTP client
    try (CloseableHttpResponse response = httpclient.execute(httpPost)) {
      // 5. Read Response
      result = EntityUtils.toString(response.getEntity());
      log.info("Status Code: " + response.getCode() + " " + response.getReasonPhrase());
    }
  } catch (IOException | ParseException e) {
    log.error(e.getMessage(), e);
  }
  return result;
}

El siguiente punto añadir un CookieStore que nos permita almacenar las cookies que nos envíe el servidor a través de la cabecera 'set-cookie'. Y que nos permita devolver dicha cookie al servidor. Muy útil para los casos en que se necesita una comunicación con el servidor, sobre todo con tareas de login.

private static CookieStore cookieStore = new BasicCookieStore();
private static CloseableHttpClient httpclient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();

public static String postWithCookieStore(final String url, final String jsonBody) {
  // 1. Create HTTP Method
  HttpPost httpPost = new HttpPost(url);
  // 2. Set payload and content-type
  httpPost.setEntity(new StringEntity(jsonBody, ContentType.APPLICATION_JSON));
  String result = null;
  try {
    // 3. Execute the method through the HTTP client
    try (CloseableHttpResponse response = httpclient.execute(httpPost)) {
      // 4. Read Response
      result = EntityUtils.toString(response.getEntity());
      log.info("Status Code: " + response.getCode() + " " + response.getReasonPhrase());
    }
  } catch (IOException | ParseException e) {
    log.error(e.getMessage(), e);
  }
  return result;
}

Ahora veremos cómo realizar una llamada con autenticación de usuario y contraseña. Y aunque siempre se puede añadir la cabecera a mano en el método HTTP, esta librería tiene sus propias clases que permiten realizar la autenticación de una forma más segura. 

public static String getWithBasicAuth(final String url, final String user, final String pass)
    throws URISyntaxException, IOException, ParseException {
  String result = null;
  URI uri = new URI(url);
  // 1. Create a Basic Credentials provider to authenticate the call
  final BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
  AuthScope authScope = new AuthScope(uri.getHost(), uri.getPort());
  credsProvider.setCredentials(authScope, new UsernamePasswordCredentials(user, pass.toCharArray()));
  // 2. Add the credentials to the HHTP Client to use it in the call
  try (final CloseableHttpClient httpclient = HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build()) {
    final HttpGet httpget = new HttpGet(url);
    try (final CloseableHttpResponse response = httpclient.execute(httpget)) {
      result = EntityUtils.toString(response.getEntity());
    }
  } catch (IOException | ParseException e) {
    log.error(e.getMessage(), e);
  }
  return result;
} 

Con BasicCredentialsProvider, el cliente web no adjuntará la cabecera de autenticación a menos que reciba un código de petición 401. Aquí podemos ver una prueba de su funcionamiento con wiremock.

final String book = "{\"name\":\"Dune\",\"author\":\"Frank Herbert\"}";
final String bookUrl = "http://localhost:57001/book";
final String basicAuth = "Basic dXNlcjpwYXNz";

@RegisterExtension
static WireMockExtension wm1 = WireMockExtension.newInstance().options(wireMockConfig().port(57001).notifier(new ConsoleNotifier(true))).build();

@Test
public void getTest(final WireMockRuntimeInfo wmRuntimeInfo) throws URISyntaxException, ParseException, IOException {
  wm1.stubFor(get("/book/1")
      .willReturn(aResponse().withStatus(401).withHeader("Connection", "keep-alive").withHeader("WWW-Authenticate", "Basic realm=\"Fake Realm\"")));
  wm1.stubFor(get("/book/1").withHeader("Authorization", equalToIgnoreCase(basicAuth)).willReturn(ok().withBody(book)));
  String result = HttpClientUtil.getWithBasicAuth(bookUrl + "/1", "user", "pass");
  assertThat(result, equalTo(book));
  wm1.verify(2, getRequestedFor(urlEqualTo("/book/1")));
  wm1.verify(1, getRequestedFor(urlEqualTo("/book/1")).withoutHeader("Authorization"));
  wm1.verify(1, getRequestedFor(urlEqualTo("/book/1")).withHeader("Authorization", equalToIgnoreCase(basicAuth)));
}

El último ejemplo será crear un cliente que invocar a cualquier endpoint con certificado autofirmado y por tanto no de confianza. No hace falta mencionar, que aunque esto es muy util en entornos de desarrollo, no se debe realizar nunca en entornos productivos

public static String trustedAllPost(final String url) {
  String result = null;
  try {
    // 1. Create SSLContextBuilder to trust in every host
    SSLContextBuilder builder = new SSLContextBuilder();
    builder.loadTrustMaterial(null, (chain, authType) -> true);
    // 2. Create a SSLConnectionSocketFactory to not verify any hostname
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(builder.build(), new NoopHostnameVerifier());
    HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create().setSSLSocketFactory(sslsf).build();
    // 3. Associate both configurations through HttpClientConnectionManager to the HTTP client
    try (CloseableHttpClient httpclient = HttpClients.custom().setConnectionManager(cm).build()) {
      HttpPost method = new HttpPost(url);
      try (CloseableHttpResponse response = httpclient.execute(method)) {
        result = EntityUtils.toString(response.getEntity());
      }
    }
  } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException | IOException | ParseException e) {
    log.error(e.getMessage(), e);
  }
  return result;
}