/*
 * Copyright 2012-2017 the original author or 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 org.springframework.boot.actuate.trace;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.security.Principal;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.junit.Test;

import org.springframework.boot.actuate.trace.TraceProperties.Include;
import org.springframework.boot.autoconfigure.web.DefaultErrorAttributes;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

/**
 * Tests for {@link WebRequestTraceFilter}.
 *
 * @author Dave Syer
 * @author Wallace Wadge
 * @author Phillip Webb
 * @author Andy Wilkinson
 * @author Venil Noronha
 * @author Stephane Nicoll
 * @author Madhura Bhave
 */
public class WebRequestTraceFilterTests {

	private final InMemoryTraceRepository repository = new InMemoryTraceRepository();

	private TraceProperties properties = new TraceProperties();

	private WebRequestTraceFilter filter = new WebRequestTraceFilter(this.repository,
			this.properties);

	@Test
	@SuppressWarnings("unchecked")
	public void filterAddsTraceWithDefaultIncludes() {
		MockHttpServletRequest request = spy(new MockHttpServletRequest("GET", "/foo"));
		request.addHeader("Accept", "application/json");
		Map<String, Object> trace = this.filter.getTrace(request);
		assertThat(trace.get("method")).isEqualTo("GET");
		assertThat(trace.get("path")).isEqualTo("/foo");
		Map<String, Object> map = (Map<String, Object>) trace.get("headers");
		assertThat(map.get("request").toString()).isEqualTo("{Accept=application/json}");
		verify(request, times(0)).getParameterMap();
	}

	@Test
	@SuppressWarnings({ "rawtypes", "unchecked" })
	public void filterAddsTraceWithCustomIncludes() throws IOException, ServletException {
		this.properties.setInclude(EnumSet.allOf(Include.class));
		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
		request.addHeader("Accept", "application/json");
		request.addHeader("Cookie", "testCookie=testValue;");
		request.setContextPath("some.context.path");
		request.setContent("Hello, World!".getBytes());
		request.setRemoteAddr("some.remote.addr");
		request.setQueryString("some.query.string");
		request.setParameter("param", "paramvalue");
		File tmp = File.createTempFile("spring-boot", "tmp");
		String url = tmp.toURI().toURL().toString();
		request.setPathInfo(url);
		tmp.deleteOnExit();
		request.setAuthType("authType");
		Principal principal = new Principal() {

			@Override
			public String getName() {
				return "principalTest";
			}

		};
		request.setUserPrincipal(principal);
		MockHttpServletResponse response = new MockHttpServletResponse();
		response.addHeader("Content-Type", "application/json");
		response.addHeader("Set-Cookie", "a=b");
		this.filter.doFilterInternal(request, response, new FilterChain() {

			@Override
			public void doFilter(ServletRequest request, ServletResponse response)
					throws IOException, ServletException {
				BufferedReader bufferedReader = request.getReader();
				while (bufferedReader.readLine() != null) {
					// read the contents as normal (forces cache to fill up)
				}
				response.getWriter().println("Goodbye, World!");
			}

		});
		assertThat(this.repository.findAll()).hasSize(1);
		Map<String, Object> trace = this.repository.findAll().iterator().next().getInfo();
		Map<String, Object> map = (Map<String, Object>) trace.get("headers");

		assertThat(map.get("response").toString())
				.isEqualTo("{Content-Type=application/json, Set-Cookie=a=b, status=200}");
		assertThat(trace.get("method")).isEqualTo("GET");
		assertThat(trace.get("path")).isEqualTo("/foo");
		assertThat(((String[]) ((Map) trace.get("parameters")).get("param"))[0])
				.isEqualTo("paramvalue");
		assertThat(trace.get("remoteAddress")).isEqualTo("some.remote.addr");
		assertThat(trace.get("query")).isEqualTo("some.query.string");
		assertThat(trace.get("userPrincipal")).isEqualTo(principal.getName());
		assertThat(trace.get("contextPath")).isEqualTo("some.context.path");
		assertThat(trace.get("pathInfo")).isEqualTo(url);
		assertThat(trace.get("authType")).isEqualTo("authType");
		assertThat(map.get("request").toString())
				.isEqualTo("{Accept=application/json, Cookie=testCookie=testValue;}");
	}

	@Test
	@SuppressWarnings({ "unchecked" })
	public void filterDoesNotAddResponseHeadersWithoutResponseHeadersInclude()
			throws ServletException, IOException {
		this.properties.setInclude(Collections.singleton(Include.REQUEST_HEADERS));
		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
		MockHttpServletResponse response = new MockHttpServletResponse();
		response.addHeader("Content-Type", "application/json");
		this.filter.doFilterInternal(request, response, new FilterChain() {
			@Override
			public void doFilter(ServletRequest request, ServletResponse response)
					throws IOException, ServletException {
			}
		});
		Map<String, Object> info = this.repository.findAll().iterator().next().getInfo();
		Map<String, Object> headers = (Map<String, Object>) info.get("headers");
		assertThat(headers.get("response") == null).isTrue();
	}

	@Test
	@SuppressWarnings({ "unchecked" })
	public void filterDoesNotAddRequestCookiesWithCookiesExclude()
			throws ServletException, IOException {
		this.properties.setInclude(Collections.singleton(Include.REQUEST_HEADERS));
		MockHttpServletRequest request = spy(new MockHttpServletRequest("GET", "/foo"));
		request.addHeader("Accept", "application/json");
		request.addHeader("Cookie", "testCookie=testValue;");
		Map<String, Object> map = (Map<String, Object>) this.filter.getTrace(request)
				.get("headers");
		assertThat(map.get("request").toString()).isEqualTo("{Accept=application/json}");
	}

	@Test
	@SuppressWarnings({ "unchecked" })
	public void filterDoesNotAddAuthorizationHeaderWithoutAuthorizationHeaderInclude()
			throws ServletException, IOException {
		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
		request.addHeader("Authorization", "my-auth-header");
		MockHttpServletResponse response = new MockHttpServletResponse();
		this.filter.doFilterInternal(request, response, new FilterChain() {

			@Override
			public void doFilter(ServletRequest request, ServletResponse response)
					throws IOException, ServletException {
			}

		});
		Map<String, Object> info = this.repository.findAll().iterator().next().getInfo();
		Map<String, Object> headers = (Map<String, Object>) info.get("headers");
		assertThat(((Map<Object, Object>) headers.get("request"))).hasSize(0);
	}

	@Test
	@SuppressWarnings({ "unchecked" })
	public void filterAddsAuthorizationHeaderWhenAuthorizationHeaderIncluded()
			throws ServletException, IOException {
		this.properties.setInclude(
				EnumSet.of(Include.REQUEST_HEADERS, Include.AUTHORIZATION_HEADER));
		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
		request.addHeader("Authorization", "my-auth-header");
		MockHttpServletResponse response = new MockHttpServletResponse();
		this.filter.doFilterInternal(request, response, new FilterChain() {

			@Override
			public void doFilter(ServletRequest request, ServletResponse response)
					throws IOException, ServletException {
			}

		});
		Map<String, Object> info = this.repository.findAll().iterator().next().getInfo();
		Map<String, Object> headers = (Map<String, Object>) info.get("headers");
		assertThat(((Map<Object, Object>) headers.get("request")))
				.containsKey("Authorization");
	}

	@Test
	@SuppressWarnings({ "unchecked" })
	public void filterDoesNotAddResponseCookiesWithCookiesExclude()
			throws ServletException, IOException {
		this.properties.setInclude(Collections.singleton(Include.RESPONSE_HEADERS));
		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
		MockHttpServletResponse response = new MockHttpServletResponse();
		response.addHeader("Content-Type", "application/json");
		response.addHeader("Set-Cookie", "testCookie=testValue;");
		Map<String, Object> trace = this.filter.getTrace(request);
		this.filter.enhanceTrace(trace, response);
		Map<String, Object> map = (Map<String, Object>) trace.get("headers");
		assertThat(map.get("response").toString())
				.isEqualTo("{Content-Type=application/json, status=200}");
	}

	@Test
	public void filterHasResponseStatus() {
		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
		MockHttpServletResponse response = new MockHttpServletResponse();
		response.setStatus(404);
		response.addHeader("Content-Type", "application/json");
		Map<String, Object> trace = this.filter.getTrace(request);
		this.filter.enhanceTrace(trace, response);
		@SuppressWarnings("unchecked")
		Map<String, Object> map = (Map<String, Object>) ((Map<String, Object>) trace
				.get("headers")).get("response");
		assertThat(map.get("status").toString()).isEqualTo("404");
	}

	@Test
	@SuppressWarnings("unchecked")
	public void filterAddsTimeTaken() throws Exception {
		MockHttpServletRequest request = spy(new MockHttpServletRequest("GET", "/foo"));
		MockHttpServletResponse response = new MockHttpServletResponse();
		MockFilterChain chain = new MockFilterChain();
		this.filter.doFilter(request, response, chain);
		String timeTaken = (String) this.repository.findAll()
				.iterator().next().getInfo().get("timeTaken");
		assertThat(timeTaken).isNotNull();
	}

	@Test
	public void filterHasError() {
		this.filter.setErrorAttributes(new DefaultErrorAttributes());
		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
		MockHttpServletResponse response = new MockHttpServletResponse();
		response.setStatus(500);
		request.setAttribute("javax.servlet.error.exception",
				new IllegalStateException("Foo"));
		response.addHeader("Content-Type", "application/json");
		Map<String, Object> trace = this.filter.getTrace(request);
		this.filter.enhanceTrace(trace, response);
		@SuppressWarnings("unchecked")
		Map<String, Object> map = (Map<String, Object>) trace.get("error");
		System.err.println(map);
		assertThat(map.get("message").toString()).isEqualTo("Foo");
	}

	@Test
	@SuppressWarnings("unchecked")
	public void filterHas500ResponseStatusWhenExceptionIsThrown()
			throws ServletException, IOException {
		MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
		MockHttpServletResponse response = new MockHttpServletResponse();

		try {
			this.filter.doFilterInternal(request, response, new FilterChain() {

				@Override
				public void doFilter(ServletRequest request, ServletResponse response)
						throws IOException, ServletException {
					throw new RuntimeException();
				}

			});
			fail("Exception was swallowed");
		}
		catch (RuntimeException ex) {
			Map<String, Object> headers = (Map<String, Object>) this.repository.findAll()
					.iterator().next().getInfo().get("headers");
			Map<String, Object> responseHeaders = (Map<String, Object>) headers
					.get("response");
			assertThat((String) responseHeaders.get("status")).isEqualTo("500");
		}
	}

	@Test
	@SuppressWarnings("unchecked")
	public void postProcessRequestHeaders() throws Exception {
		this.filter = new WebRequestTraceFilter(this.repository, this.properties) {

			@Override
			protected void postProcessRequestHeaders(Map<String, Object> headers) {
				headers.remove("Test");
			}

		};
		MockHttpServletRequest request = spy(new MockHttpServletRequest("GET", "/foo"));
		request.addHeader("Accept", "application/json");
		request.addHeader("Test", "spring");
		Map<String, Object> map = (Map<String, Object>) this.filter.getTrace(request)
				.get("headers");
		assertThat(map.get("request").toString()).isEqualTo("{Accept=application/json}");
	}

}
