// Copyright 2014 Google Inc. 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.google.api.ads.adwords.lib.utils.logging; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThat; 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.google.api.client.http.GenericUrl; import com.google.api.client.http.HttpContent; import com.google.api.client.http.HttpStatusCodes; import com.google.api.client.http.UrlEncodedContent; import com.google.api.client.util.GenericData; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.hamcrest.Matchers; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.slf4j.Logger; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; /** * Tests for {@link ReportServiceLogger}. */ @RunWith(JUnit4.class) public class ReportServiceLoggerTest { @Mock private Logger reportLogger; // Default values for various request/response attributes. These will all be set to some // non-null, meaningful value. private HttpContent defaultRequestContent; private GenericData defaultRequestHeaders; private Set<String> defaultExpectedHeaders; private Set<String> defaultRedactedHeaders; private GenericUrl defaultUrl; private String defaultRequestMethod; /** * ReportServiceLogger being tested. */ private ReportServiceLogger reportServiceLogger; /** * Sets all instance variables to values for a successful request. Tests that require failed * requests or null/empty values should mutate the instance variables accordingly. */ @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); defaultRequestMethod = "GET"; defaultUrl = new GenericUrl("http://www.foo.com/bar"); reportServiceLogger = new ReportServiceLogger(reportLogger); // Constructs the request headers. Map<String, String> data = Maps.newHashMap(); // Adds headers that should be scrubbed. for (String scrubbedHeader : ReportServiceLogger.SCRUBBED_HEADERS) { data.put(scrubbedHeader, "foo" + scrubbedHeader); // Also add the header in uppercase to confirm it will be scrubbed, regardless of case. data.put(scrubbedHeader.toUpperCase(), "fooUpper" + scrubbedHeader); } defaultRedactedHeaders = Sets.newHashSet(data.keySet()); // Adds headers that should not be scrubbed. data.put("ClientCustomerId", "123-456-7890"); data.put("SomeOtherHeader", "SomeOtherValue"); defaultExpectedHeaders = Sets.newHashSet(data.keySet()); defaultExpectedHeaders.removeAll(defaultRedactedHeaders); defaultRequestHeaders = new GenericData(); defaultRequestHeaders.putAll(data); defaultRequestContent = new UrlEncodedContent(data); } @Test public void testLogRequest_nullRequestMethod() { when(reportLogger.isWarnEnabled()).thenReturn(true); assertRequestDetailsLogged(false, null, defaultUrl, defaultRequestContent, defaultRequestHeaders, defaultExpectedHeaders, defaultRedactedHeaders); } @Test public void testLogRequest_nullUrl() { when(reportLogger.isWarnEnabled()).thenReturn(true); assertRequestDetailsLogged(false, defaultRequestMethod, null, defaultRequestContent, defaultRequestHeaders, defaultExpectedHeaders, defaultRedactedHeaders); } @Test public void testLogRequest_nullContent() { when(reportLogger.isWarnEnabled()).thenReturn(true); assertRequestDetailsLogged(false, defaultRequestMethod, defaultUrl, null, defaultRequestHeaders, defaultExpectedHeaders, defaultRedactedHeaders); } @Test public void testLogRequest_nullHeaders() { when(reportLogger.isWarnEnabled()).thenReturn(true); assertRequestDetailsLogged(false, defaultRequestMethod, defaultUrl, defaultRequestContent, defaultRequestHeaders, defaultExpectedHeaders, defaultRedactedHeaders); } @Test public void testLogResponse_nullStatusMessage() { when(reportLogger.isWarnEnabled()).thenReturn(true); assertResponseDetailsLogged(HttpStatusCodes.STATUS_CODE_FORBIDDEN, null, false); } /** * Tests that failed request details are logged with WARN level if warn is enabled. */ @Test public void testLogFailedRequest_warnEnabled() { when(reportLogger.isWarnEnabled()).thenReturn(true); assertRequestDetailsLogged(false, defaultRequestMethod, defaultUrl, defaultRequestContent, defaultRequestHeaders, defaultExpectedHeaders, defaultRedactedHeaders); } /** * Tests that failed request details are NOT logged with WARN level if warn is disabled. */ @Test public void testLogFailedRequest_warnDisabled() { when(reportLogger.isWarnEnabled()).thenReturn(false); reportServiceLogger.logRequest(defaultRequestMethod, defaultUrl, defaultRequestContent, defaultRequestHeaders, false); verify(reportLogger, never()).warn(org.mockito.Matchers.anyString()); } @Test public void testLogFailedResponse_warnEnabled() { when(reportLogger.isWarnEnabled()).thenReturn(true); assertResponseDetailsLogged(HttpStatusCodes.STATUS_CODE_SERVER_ERROR, "FAILED", false); } /** * Tests that failed response details are NOT logged with WARN level if warn is disabled. */ @Test public void testLogFailedResponse_warnDisabled() { when(reportLogger.isWarnEnabled()).thenReturn(false); reportServiceLogger.logResponse(HttpStatusCodes.STATUS_CODE_BAD_GATEWAY, "Failure", false); verify(reportLogger, never()).warn(org.mockito.Matchers.anyString()); } /** * Tests that successful request details are logged with INFO level if info is enabled. */ @Test public void testLogSuccessfulRequest_infoEnabled() { when(reportLogger.isInfoEnabled()).thenReturn(true); assertRequestDetailsLogged(true, defaultRequestMethod, defaultUrl, defaultRequestContent, defaultRequestHeaders, defaultExpectedHeaders, defaultRedactedHeaders); } /** * Tests that successful request details are NOT logged with INFO level if info is disabled. */ @Test public void testLogSuccessfulRequest_infoDisabled() { when(reportLogger.isInfoEnabled()).thenReturn(false); reportServiceLogger.logRequest(defaultRequestMethod, defaultUrl, defaultRequestContent, defaultRequestHeaders, true); verify(reportLogger, never()).info(org.mockito.Matchers.anyString()); } @Test public void testLogSuccessfulResponse_infoEnabled() { when(reportLogger.isInfoEnabled()).thenReturn(true); assertResponseDetailsLogged(HttpStatusCodes.STATUS_CODE_OK, "SUCCESS", true); } /** * Tests that successful response details are NOT logged with INFO level if info is disabled. */ @Test public void testLogSuccessfulResponse_infoDisabled() { when(reportLogger.isInfoEnabled()).thenReturn(false); reportServiceLogger.logResponse(HttpStatusCodes.STATUS_CODE_OK, "Status is OK", true); verify(reportLogger, never()).info(org.mockito.Matchers.anyString()); } @Test public void testGetLogger() { assertNotNull("getLogger should return a non-null Logger", reportServiceLogger.getLogger()); assertSame("getLogger should return the Logger passed to its constructor", reportLogger, reportServiceLogger.getLogger()); } /** * Asserts that all of the request details were logged. * * @param isSuccessful if the request was successful * @param requestMethod HTTP request method of the request * @param url URL of the request * @param requestContent content of the request * @param requestHeaders headers on the request * @param expectedHeaders headers whose values should appear in log messages * @param redactedHeaders headers whose values should be redacted in log messages */ private void assertRequestDetailsLogged(boolean isSuccessful, String requestMethod, GenericUrl url, HttpContent requestContent, GenericData requestHeaders, Set<String> expectedHeaders, Set<String> redactedHeaders) { // Invokes the report service logger so calls on it and its underlying SLF4J logger can // be verified. reportServiceLogger.logRequest(requestMethod, url, requestContent, requestHeaders, isSuccessful); // Verifies invocations and arguments. ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class); if (isSuccessful) { verify(reportLogger, times(2)).info(messageCaptor.capture()); } else { verify(reportLogger, times(2)).warn(messageCaptor.capture()); } List<String> capturedMessages = messageCaptor.getAllValues(); assertEquals("Number of captured messages is incorrect", 2, capturedMessages.size()); if (url != null) { assertThat("URL not logged", capturedMessages.get(0), containsString(url.build())); } if (requestMethod != null) { assertThat("Request method not logged", capturedMessages.get(0), containsString(requestMethod)); } if (requestHeaders != null) { // Verifies that expected headers were logged with values and redacted headers were logged // with masked values. for (Entry<String, Object> headerEntry : requestHeaders.entrySet()) { String header = headerEntry.getKey(); Object value = headerEntry.getValue(); assertThat("Expected header name not logged", capturedMessages.get(1), containsString(header)); Object expectedValue; if (expectedHeaders.contains(header)) { assertThat("Expected header value not logged", capturedMessages.get(1), containsString(value.toString())); expectedValue = value; } else { assertThat("Header is in neither redactedHeaders nor expectedHeaders - confirm " + "this test is set up correctly", redactedHeaders, Matchers.hasItem(header)); assertThat("Redacted header value logged when it shouldn't be", capturedMessages.get(1), not(Matchers.containsString(value.toString()))); expectedValue = ReportServiceLogger.SCRUBBED_HEADERS_VALUE; } assertThat("Header: value does not match expectatations", capturedMessages.get(1), Matchers.containsString(String.format("%s: %s", header, expectedValue))); } } } /** * Asserts that all of the response details were logged. */ private void assertResponseDetailsLogged(int statusCode, String statusMessage, boolean isSuccessful) { // Invokes the report service logger so calls on it and its underlying SLF4J logger can // be verified. reportServiceLogger.logResponse(statusCode, statusMessage, isSuccessful); // Verifies invocations and arguments. ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class); if (isSuccessful) { verify(reportLogger, times(1)).info(messageCaptor.capture()); } else { verify(reportLogger, times(1)).warn(messageCaptor.capture()); } assertEquals("Number of captured messages is incorrect", 1, messageCaptor.getAllValues().size()); String capturedMessage = messageCaptor.getValue(); assertThat("Status code not logged", capturedMessage, containsString(String.valueOf(statusCode))); if (statusMessage != null) { assertThat("Status message not logged", capturedMessage, containsString(statusMessage)); } } }