Java tutorial
/* * Copyright 2016-2018 Stefan Kalscheuer * * 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 de.stklcode.jvault.connector; import de.stklcode.jvault.connector.exception.InvalidRequestException; import de.stklcode.jvault.connector.exception.InvalidResponseException; import de.stklcode.jvault.connector.exception.PermissionDeniedException; import de.stklcode.jvault.connector.exception.VaultConnectorException; import net.bytebuddy.ByteBuddy; import net.bytebuddy.agent.ByteBuddyAgent; import net.bytebuddy.dynamic.loading.ClassReloadingStrategy; import org.apache.http.ProtocolVersion; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicStatusLine; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Collections; import static net.bytebuddy.implementation.MethodDelegation.to; import static net.bytebuddy.matcher.ElementMatchers.named; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.nullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; /** * JUnit test for HTTP Vault connector. * This test suite contains tests that do not require connection to an actual Vault instance. * * @author Stefan Kalscheuer * @since 0.7.0 */ public class HTTPVaultConnectorOfflineTest { private static final String INVALID_URL = "foo:/\\1nv4l1d_UrL"; private static CloseableHttpClient httpMock = mock(CloseableHttpClient.class); private CloseableHttpResponse responseMock = mock(CloseableHttpResponse.class); @BeforeAll public static void initByteBuddy() { // Install ByteBuddy Agent. ByteBuddyAgent.install(); } /** * Helper method for redefinition of {@link HttpClientBuilder#create()} from {@link #initHttpMock()}. * * @return Mocked HTTP client builder. */ public static HttpClientBuilder create() { return new MockedHttpClientBuilder(); } @BeforeEach public void initHttpMock() { // Redefine static method to return Mock on HttpClientBuilder creation. new ByteBuddy().redefine(HttpClientBuilder.class).method(named("create")) .intercept(to(HTTPVaultConnectorOfflineTest.class)).make() .load(HttpClientBuilder.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent()); // Re-initialize HTTP mock to ensure fresh (empty) results. httpMock = mock(CloseableHttpClient.class); } /** * Test exceptions thrown during request. */ @Test public void requestExceptionTest() throws IOException { HTTPVaultConnector connector = new HTTPVaultConnector("http://127.0.0.1", null, 0, 250); // Test invalid response code. final int responseCode = 400; mockResponse(responseCode, "", ContentType.APPLICATION_JSON); try { connector.getHealth(); fail("Querying health status succeeded on invalid instance"); } catch (Exception e) { assertThat("Unexpected type of exception", e, instanceOf(InvalidResponseException.class)); assertThat("Unexpected exception message", e.getMessage(), is("Invalid response code")); assertThat("Unexpected status code in exception", ((InvalidResponseException) e).getStatusCode(), is(responseCode)); assertThat("Response message where none was expected", ((InvalidResponseException) e).getResponse(), is(nullValue())); } // Simulate permission denied response. mockResponse(responseCode, "{\"errors\":[\"permission denied\"]}", ContentType.APPLICATION_JSON); try { connector.getHealth(); fail("Querying health status succeeded on invalid instance"); } catch (Exception e) { assertThat("Unexpected type of exception", e, instanceOf(PermissionDeniedException.class)); } // Test exception thrown during request. when(httpMock.execute(any())).thenThrow(new IOException("Test Exception")); try { connector.getHealth(); fail("Querying health status succeeded on invalid instance"); } catch (Exception e) { assertThat("Unexpected type of exception", e, instanceOf(InvalidResponseException.class)); assertThat("Unexpected exception message", e.getMessage(), is("Unable to read response")); assertThat("Unexpected cause", e.getCause(), instanceOf(IOException.class)); } // Now simulate a failing request that succeeds on second try. connector = new HTTPVaultConnector("https://127.0.0.1", null, 1, 250); doReturn(responseMock).doReturn(responseMock).when(httpMock).execute(any()); doReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 500, "")) .doReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 500, "")) .doReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 500, "")) .doReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "")).when(responseMock) .getStatusLine(); when(responseMock.getEntity()).thenReturn(new StringEntity("{}", ContentType.APPLICATION_JSON)); try { connector.getHealth(); } catch (Exception e) { fail("Request failed unexpectedly: " + e.getMessage()); } } /** * Test constductors of the {@link HTTPVaultConnector} class. */ @Test public void constructorTest() throws IOException, CertificateException { final String url = "https://vault.example.net/test/"; final String hostname = "vault.example.com"; final Integer port = 1337; final String prefix = "/custom/prefix/"; final Integer retries = 42; final String expectedNoTls = "http://" + hostname + "/v1/"; final String expectedCustomPort = "https://" + hostname + ":" + port + "/v1/"; final String expectedCustomPrefix = "https://" + hostname + ":" + port + prefix; X509Certificate trustedCaCert; try (InputStream is = getClass().getResourceAsStream("/tls/ca.pem")) { trustedCaCert = (X509Certificate) CertificateFactory.getInstance("X.509").generateCertificate(is); } // Most basic constructor expects complete URL. HTTPVaultConnector connector = new HTTPVaultConnector(url); assertThat("Unexpected base URL", getPrivate(connector, "baseURL"), is(url)); // Now override TLS usage. connector = new HTTPVaultConnector(hostname, false); assertThat("Unexpected base URL with TLS disabled", getPrivate(connector, "baseURL"), is(expectedNoTls)); // Specify custom port. connector = new HTTPVaultConnector(hostname, true, port); assertThat("Unexpected base URL with custom port", getPrivate(connector, "baseURL"), is(expectedCustomPort)); // Specify custom prefix. connector = new HTTPVaultConnector(hostname, true, port, prefix); assertThat("Unexpected base URL with custom prefix", getPrivate(connector, "baseURL"), is(expectedCustomPrefix)); assertThat("Trusted CA cert set, but not specified", getPrivate(connector, "trustedCaCert"), is(nullValue())); // Provide custom SSL context. connector = new HTTPVaultConnector(hostname, true, port, prefix, trustedCaCert); assertThat("Unexpected base URL with custom prefix", getPrivate(connector, "baseURL"), is(expectedCustomPrefix)); assertThat("Trusted CA cert not filled correctly", getPrivate(connector, "trustedCaCert"), is(trustedCaCert)); // Specify number of retries. connector = new HTTPVaultConnector(url, trustedCaCert, retries); assertThat("Number of retries not set correctly", getPrivate(connector, "retries"), is(retries)); // Test TLS version (#22). assertThat("TLS version should be 1.2 if not specified", getPrivate(connector, "tlsVersion"), is("TLSv1.2")); // Now override. connector = new HTTPVaultConnector(url, trustedCaCert, retries, null, "TLSv1.1"); assertThat("Overridden TLS version 1.1 not correct", getPrivate(connector, "tlsVersion"), is("TLSv1.1")); } /** * This test is designed to test exceptions caught and thrown by seal-methods if Vault is not reachable. */ @Test public void sealExceptionTest() throws IOException { HTTPVaultConnector connector = new HTTPVaultConnector(INVALID_URL); try { connector.sealStatus(); fail("Querying seal status succeeded on invalid URL"); } catch (Exception e) { assertThat("Unexpected type of exception", e, instanceOf(InvalidRequestException.class)); assertThat("Unexpected exception message", e.getMessage(), is("Invalid URI format")); } connector = new HTTPVaultConnector("https://127.0.0.1", null, 0, 250); // Simulate NULL response (mock not supplied with data). try { connector.sealStatus(); fail("Querying seal status succeeded on invalid instance"); } catch (Exception e) { assertThat("Unexpected type of exception", e, instanceOf(InvalidResponseException.class)); assertThat("Unexpected exception message", e.getMessage(), is("Response unavailable")); } } /** * This test is designed to test exceptions caught and thrown by seal-methods if Vault is not reachable. */ @Test public void healthExceptionTest() throws IOException { HTTPVaultConnector connector = new HTTPVaultConnector(INVALID_URL); try { connector.getHealth(); fail("Querying health status succeeded on invalid URL"); } catch (Exception e) { assertThat("Unexpected type of exception", e, instanceOf(InvalidRequestException.class)); assertThat("Unexpected exception message", e.getMessage(), is("Invalid URI format")); } connector = new HTTPVaultConnector("https://127.0.0.1", null, 0, 250); // Simulate NULL response (mock not supplied with data). try { connector.getHealth(); fail("Querying health status succeeded on invalid instance"); } catch (Exception e) { assertThat("Unexpected type of exception", e, instanceOf(InvalidResponseException.class)); assertThat("Unexpected exception message", e.getMessage(), is("Response unavailable")); } } /** * Test behavior on unparsable responses. */ @Test public void parseExceptionTest() throws IOException { HTTPVaultConnector connector = new HTTPVaultConnector("https://127.0.0.1", null, 0, 250); // Mock authorization. setPrivate(connector, "authorized", true); // Mock response. mockResponse(200, "invalid", ContentType.APPLICATION_JSON); // Now test the methods. try { connector.sealStatus(); fail("sealStatus() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } try { connector.unseal("key"); fail("unseal() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } try { connector.getHealth(); fail("getHealth() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } try { connector.getAuthBackends(); fail("getAuthBackends() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } try { connector.authToken("token"); fail("authToken() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } try { connector.lookupAppRole("roleName"); fail("lookupAppRole() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } try { connector.getAppRoleID("roleName"); fail("getAppRoleID() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } try { connector.createAppRoleSecret("roleName"); fail("createAppRoleSecret() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } try { connector.lookupAppRoleSecret("roleName", "secretID"); fail("lookupAppRoleSecret() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } try { connector.listAppRoles(); fail("listAppRoles() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } try { connector.listAppRoleSecrets("roleName"); fail("listAppRoleSecrets() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } try { connector.read("key"); fail("read() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } try { connector.list("path"); fail("list() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } try { connector.renew("leaseID"); fail("renew() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } try { connector.lookupToken("token"); fail("lookupToken() succeeded on invalid instance"); } catch (Exception e) { assertParseError(e); } } private void assertParseError(Exception e) { assertThat("Unexpected type of exception", e, instanceOf(InvalidResponseException.class)); assertThat("Unexpected exception message", e.getMessage(), is("Unable to parse response")); } /** * Test requests that expect an empty response with code 204, but receive a 200 body. */ @Test public void nonEmpty204ResponseTest() throws IOException { HTTPVaultConnector connector = new HTTPVaultConnector("https://127.0.0.1", null, 0, 250); // Mock authorization. setPrivate(connector, "authorized", true); // Mock response. mockResponse(200, "{}", ContentType.APPLICATION_JSON); // Now test the methods expecting a 204. try { connector.registerAppId("appID", "policy", "displayName"); fail("registerAppId() with 200 response succeeded"); } catch (VaultConnectorException e) { assertThat("Unexpected exception type", e, instanceOf(InvalidResponseException.class)); } try { connector.registerUserId("appID", "userID"); fail("registerUserId() with 200 response succeeded"); } catch (VaultConnectorException e) { assertThat("Unexpected exception type", e, instanceOf(InvalidResponseException.class)); } try { connector.createAppRole("appID", Collections.singletonList("policy")); fail("createAppRole() with 200 response succeeded"); } catch (VaultConnectorException e) { assertThat("Unexpected exception type", e, instanceOf(InvalidResponseException.class)); } try { connector.deleteAppRole("roleName"); fail("deleteAppRole() with 200 response succeeded"); } catch (VaultConnectorException e) { assertThat("Unexpected exception type", e, instanceOf(InvalidResponseException.class)); } try { connector.setAppRoleID("roleName", "roleID"); fail("setAppRoleID() with 200 response succeeded"); } catch (VaultConnectorException e) { assertThat("Unexpected exception type", e, instanceOf(InvalidResponseException.class)); } try { connector.destroyAppRoleSecret("roleName", "secretID"); fail("destroyAppRoleSecret() with 200 response succeeded"); } catch (VaultConnectorException e) { assertThat("Unexpected exception type", e, instanceOf(InvalidResponseException.class)); } try { connector.destroyAppRoleSecret("roleName", "secretUD"); fail("destroyAppRoleSecret() with 200 response succeeded"); } catch (VaultConnectorException e) { assertThat("Unexpected exception type", e, instanceOf(InvalidResponseException.class)); } try { connector.delete("key"); fail("delete() with 200 response succeeded"); } catch (VaultConnectorException e) { assertThat("Unexpected exception type", e, instanceOf(InvalidResponseException.class)); } try { connector.revoke("leaseID"); fail("destroyAppRoleSecret() with 200 response succeeded"); } catch (VaultConnectorException e) { assertThat("Unexpected exception type", e, instanceOf(InvalidResponseException.class)); } } private Object getPrivate(Object target, String fieldName) { try { Field field = target.getClass().getDeclaredField(fieldName); if (field.isAccessible()) return field.get(target); field.setAccessible(true); Object value = field.get(target); field.setAccessible(false); return value; } catch (NoSuchFieldException | IllegalAccessException e) { return null; } } private void setPrivate(Object target, String fieldName, Object value) { try { Field field = target.getClass().getDeclaredField(fieldName); boolean accessible = field.isAccessible(); field.setAccessible(true); field.set(target, value); field.setAccessible(accessible); } catch (NoSuchFieldException | IllegalAccessException e) { // Should not occur, to be taken care of in test code. } } private void mockResponse(int status, String body, ContentType type) throws IOException { when(httpMock.execute(any())).thenReturn(responseMock); when(responseMock.getStatusLine()) .thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), status, "")); when(responseMock.getEntity()).thenReturn(new StringEntity(body, type)); } /** * Mocked {@link HttpClientBuilder} that always returns the mocked client. */ private static class MockedHttpClientBuilder extends HttpClientBuilder { @Override public CloseableHttpClient build() { return httpMock; } } }