/* * Copyright © 2016, 2018 IBM Corp. All rights reserved. * * 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.cloudant.tests; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import com.cloudant.client.api.ClientBuilder; import com.cloudant.client.api.CloudantClient; import com.cloudant.http.Http; import com.cloudant.http.HttpConnection; import com.cloudant.tests.extensions.MockWebServerExtension; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import mockit.Expectations; import mockit.Mocked; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import java.io.ByteArrayInputStream; import java.net.URL; import java.security.Security; import java.security.SecurityPermission; import java.util.ArrayList; import java.util.List; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.regex.Pattern; public class LoggingTest { private static final String logPrefixPattern = "[0-9,a-f]+-\\d+ "; @RegisterExtension public static MockWebServerExtension mockWebServerExt = new MockWebServerExtension(); public static MockWebServer mockWebServer; private static CloudantClient client; private volatile Logger logger; private VerificationLogHandler handler; @BeforeEach public void setupMockWebServer() throws Exception { mockWebServer = mockWebServerExt.get(); // Set a dispatcher that always returns 200 OK mockWebServer.setDispatcher(new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { return new MockResponse(); } }); client = CloudantClientHelper.newMockWebServerClientBuilder(mockWebServer) .build(); } @BeforeEach public void createHandler() { handler = new VerificationLogHandler(); } @AfterEach public void teardown() throws Exception { // Remove the handler from the logger logger.removeHandler(handler); // Reload whatever log configuration existed before the test LogManager.getLogManager().readConfiguration(); } @Test public void httpLoggingEnabled() throws Exception { logger = setupLogger(HttpConnection.class, Level.ALL); client.executeRequest(Http.GET(client.getBaseUri())).responseAsString(); assertTrue(handler.logEntries.size() > 0, "There should be at least 1 log entry"); } @Test public void urlRegexLogging() throws Exception { //Set the regex filter property on the LogManager and assert it was set String urlFilterPropName = "com.cloudant.http.filter.url"; String urlFilterPropValue = ".*/testdb.*"; setAndAssertLogProperty(urlFilterPropName, urlFilterPropValue); // Configure HttpConnection logging and get a client logger = setupLogger(HttpConnection.class, Level.FINE); // Make a request to testdb client.executeRequest(Http.GET(new URL(client.getBaseUri().toString() + "/testdb"))) .responseAsString(); // Check there were two log messages one for request and one for response assertEquals(2, handler.logEntries.size(), "There should be 2 log messages"); // Check the messages were the ones we expected assertHttpMessage("GET .*/testdb request", 0); assertHttpMessage("GET .*/testdb response 200 OK", 1); // Store the current log size int logsize = handler.logEntries.size(); // Make a second request to a different URL and check that nothing else was logged client.executeRequest(Http.GET(client.getBaseUri())).responseAsString(); assertEquals(logsize, handler.logEntries.size(), "There should have been no more log " + "entries"); } private String methodFilterPropName = "com.cloudant.http.filter.method"; @Test public void httpMethodFilterLogging() throws Exception { setAndAssertLogProperty(methodFilterPropName, "GET"); logger = setupLogger(HttpConnection.class, Level.FINE); // Make a GET request client.executeRequest(Http.GET(client.getBaseUri())).responseAsString(); // Check there were two log messages one for request and one for response assertEquals(2, handler.logEntries.size(), "There should be 2 log messages"); // Check the messages were the ones we expected assertHttpMessage("GET .* request", 0); assertHttpMessage("GET .* response 200 OK", 1); // Store the current log size int logsize = handler.logEntries.size(); // Make a PUT request to a different URL and check that nothing else was logged client.executeRequest(Http.PUT(client.getBaseUri(), "text/plain").setRequestBody("")) .responseAsString(); assertEquals(logsize, handler.logEntries.size(), "There should have been no more log " + "entries"); } @Test public void httpMethodFilterLoggingList() throws Exception { setAndAssertLogProperty(methodFilterPropName, "PUT,GET"); logger = setupLogger(HttpConnection.class, Level.FINE); // Make a GET request client.executeRequest(Http.GET(client.getBaseUri())).responseAsString(); // Check there were two log messages one for request and one for response assertEquals(2, handler.logEntries.size(), "There should be 2 log messages"); // Check the messages were the ones we expected assertHttpMessage("GET .* request", 0); assertHttpMessage("GET .* response 200 OK", 1); // Make a PUT request to a different URL and check that new messages are logged client.executeRequest(Http.PUT(client.getBaseUri(), "text/plain").setRequestBody("")) .responseAsString(); assertEquals(4, handler.logEntries.size(), "There should now be 4 log messages"); // Check the messages were the ones we expected assertHttpMessage("PUT .* request", 2); assertHttpMessage("PUT .* response 200 OK", 3); } @Test public void clientBuilderLogging() throws Exception { logger = setupLogger(ClientBuilder.class, Level.CONFIG); CloudantClientHelper.newMockWebServerClientBuilder(mockWebServer).build(); assertEquals(5, handler.logEntries.size(), "There should be 5 log entries"); // Validate each of the 5 entries are what we expect assertLogMessage("URL: .*", 0); assertLogMessage("Building client using URL: .*", 1); assertLogMessage("Connect timeout: .*", 2); assertLogMessage("Read timeout: .*", 3); assertLogMessage("Using default GSON builder", 4); } /** * A basic DNS log test that can be called with different values. * * @param cacheValue the value to set for the cache lifetime * @throws Exception if the test fails or errors */ private void basicDnsLogTest(String cacheValue) throws Exception { logger = setupLogger(ClientBuilder.class, Level.WARNING); String previous = Security.getProperty("networkaddress.cache.ttl"); try { Security.setProperty("networkaddress.cache.ttl", cacheValue); CloudantClientHelper.getClientBuilder().build(); } finally { // No way to unset a property, just reset to previous value // or set to 30 if it was null Security.setProperty("networkaddress.cache.ttl", (previous == null) ? "30" : previous); } } /** * Test that no warning is logged if the DNS lifetime is less than 30 s * * @throws Exception */ @Test public void dnsNoWarningLessThan30() throws Exception { basicDnsLogTest("29"); // Assert no warning was received assertEquals(0, handler.logEntries.size(), "There should be no log entry"); } /** * Test that no warning is logged if DNS caching is disabled * * @throws Exception */ @Test public void dnsNoWarning0() throws Exception { basicDnsLogTest("0"); // Assert no warning was received assertEquals(0, handler.logEntries.size(), "There should be no log entry"); } /** * Test that a warning is logged if DNS caching is set to cache forever * * @throws Exception */ @Test public void dnsWarningForever() throws Exception { basicDnsLogTest("-1"); // Assert a warning was received assertEquals(1, handler.logEntries.size(), "There should be 1 log entry"); // Assert that it matches the expected pattern assertLogMessage("DNS cache lifetime may be too long\\. .*", 0); } /** * Test that no warning is logged if the DNS lifetime is 30 s * * @throws Exception */ @Test public void dnsNoWarning30() throws Exception { basicDnsLogTest("30"); // Assert no warning was received assertEquals(0, handler.logEntries.size(), "There should be no log entry"); } /** * Test that a warning is logged if the DNS lifetime is longer than 30 s * * @throws Exception */ @Test public void dnsWarning31() throws Exception { basicDnsLogTest("31"); // Assert a warning was received assertEquals(1, handler.logEntries.size(), "There should be 1 log entry"); // Assert that it matches the expected pattern assertLogMessage("DNS cache lifetime may be too long\\. .*", 0); } /** * Test that a warning is logged if the DNS lifetime cannot be checked because of security * permissions. * * @throws Exception */ @Test public void dnsWarningPermissionDenied(@Mocked final SecurityManager mockSecurityManager) throws Exception { // Record the mock expectations new Expectations() { { mockSecurityManager.checkPermission(new SecurityPermission("getProperty" + ".networkaddress.cache.ttl")); result = new SecurityException("Test exception to deny property access."); times = 1; } }; logger = setupLogger(ClientBuilder.class, Level.WARNING); try { System.setSecurityManager(mockSecurityManager); CloudantClientHelper.getClientBuilder().build(); } finally { // Unset the mock security manager System.setSecurityManager(null); } // Assert a warning was received assertEquals(1, handler.logEntries.size(), "There should be 1 log entry"); // Assert that it matches the expected pattern assertLogMessage("Permission denied to check Java DNS cache TTL\\. .*", 0); } /** * Test that a warning is logged if a security manager is in use and the DNS cache lifetime * property is unset. * * @throws Exception */ @Test public void dnsWarningDefaultWithSecurityManager(@Mocked final SecurityManager mockSecurityManager) throws Exception { // Record the mock expectations new Expectations() { { mockSecurityManager.checkPermission(new SecurityPermission("getProperty" + ".networkaddress.cache.ttl")); minTimes = 2; // Once to set, once to get, and once to reset maxTimes = 3; // Possible third call to reset the value, depending on test ordering } }; try { System.setSecurityManager(mockSecurityManager); // We can't set null as a value and there are no APIs for clearing a value. Another test // may already have changed the value so we just set it to something invalid "a" to get // a default value. basicDnsLogTest("a"); } finally { // Unset the mock security manager System.setSecurityManager(null); } // Assert a warning was received assertEquals(1, handler.logEntries.size(), "There should be 1 log entry"); // Assert that it matches the expected pattern assertLogMessage("DNS cache lifetime may be too long\\. .*", 0); } /** * Set a LogManager configuration property and assert it was set correctly */ private void setAndAssertLogProperty(String name, String value) throws Exception { LogManager.getLogManager().readConfiguration(new ByteArrayInputStream((name + "=" + value).getBytes())); assertEquals(value, LogManager.getLogManager().getProperty(name), "The log property " + "should be the test value"); } /** * Assert a HTTP log message matches the prefix pattern plus a suffix * * @param pattern the regex pattern to check the message matches * @param index the index of the message in the handler list */ private void assertLogMessage(String pattern, int index) { Pattern p = Pattern.compile(pattern); String msg = handler.logEntries.get(index).getMessage(); assertTrue(p.matcher(msg).matches(), "The log entry \"" + msg + "\" should match pattern " + "" + pattern); } /** * Assert a HTTP log message matches the prefix pattern plus a suffix * * @param logMsgSuffixPattern the suffix part of the pattern * @param index the index of the message in the handler list */ private void assertHttpMessage(String logMsgSuffixPattern, int index) { assertLogMessage(logPrefixPattern + logMsgSuffixPattern, index); } /** * A java.util.logging.Handler that just adds log entries to a list in memory so they can be * verified by the tests. */ private static final class VerificationLogHandler extends Handler { public List<LogRecord> logEntries = new ArrayList<LogRecord>(); VerificationLogHandler() { // Default to no logging this.setLevel(Level.OFF); } @Override public void publish(LogRecord record) { logEntries.add(record); } @Override public void flush() { // No-op } @Override public void close() throws SecurityException { // No-op } } /** * A method to get and initialize a logger to test for a given class. * * @param classToLog the class we want a logger for * @param level the logging level to enable * @return the logger * @throws Exception if something goes wrong */ private Logger setupLogger(Class<?> classToLog, Level level) throws Exception { String loggerName = classToLog.getName(); // Get the logger and assert non-null Logger l = Logger.getLogger(loggerName); // Add the verification handler l.addHandler(handler); // Set the logging level for the test l.setLevel(level); handler.setLevel(level); return l; } }