/*
 * Copyright 2012-2016 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.security.web.servlet.util.matcher;

import java.util.Collections;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import org.springframework.web.servlet.handler.MatchableHandlerMapping;
import org.springframework.web.servlet.handler.RequestMatchResult;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;

/**
 * @author Rob Winch
 */
@RunWith(MockitoJUnitRunner.class)
public class MvcRequestMatcherTests {
	@Mock
	HandlerMappingIntrospector introspector;
	@Mock
	MatchableHandlerMapping mapping;
	@Mock
	RequestMatchResult result;
	@Captor
	ArgumentCaptor<String> pattern;
	MockHttpServletRequest request;

	MvcRequestMatcher matcher;

	@Before
	public void setup() throws Exception {
		this.request = new MockHttpServletRequest();
		this.request.setMethod("GET");
		this.request.setServletPath("/path");
		this.matcher = new MvcRequestMatcher(this.introspector, "/path");
	}

	@Test
	public void extractUriTemplateVariablesSuccess() throws Exception {
		when(this.result.extractUriTemplateVariables())
				.thenReturn(Collections.singletonMap("p", "path"));
		when(this.introspector.getMatchableHandlerMapping(this.request))
				.thenReturn(this.mapping);
		when(this.mapping.match(eq(this.request), this.pattern.capture()))
				.thenReturn(this.result);

		this.matcher = new MvcRequestMatcher(this.introspector, "/{p}");
		when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null);

		assertThat(this.matcher.extractUriTemplateVariables(this.request))
				.containsEntry("p", "path");
	}

	@Test
	public void extractUriTemplateVariablesFail() throws Exception {
		when(this.result.extractUriTemplateVariables())
				.thenReturn(Collections.<String, String>emptyMap());
		when(this.introspector.getMatchableHandlerMapping(this.request))
				.thenReturn(this.mapping);
		when(this.mapping.match(eq(this.request), this.pattern.capture()))
				.thenReturn(this.result);

		assertThat(this.matcher.extractUriTemplateVariables(this.request)).isEmpty();
	}

	@Test
	public void extractUriTemplateVariablesDefaultSuccess() throws Exception {
		this.matcher = new MvcRequestMatcher(this.introspector, "/{p}");
		when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null);

		assertThat(this.matcher.extractUriTemplateVariables(this.request))
				.containsEntry("p", "path");
	}

	@Test
	public void extractUriTemplateVariablesDefaultFail() throws Exception {
		this.matcher = new MvcRequestMatcher(this.introspector, "/nomatch/{p}");
		when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null);

		assertThat(this.matcher.extractUriTemplateVariables(this.request)).isEmpty();
	}

	@Test
	public void matchesServletPathTrue() throws Exception {
		when(this.introspector.getMatchableHandlerMapping(this.request))
				.thenReturn(this.mapping);
		when(this.mapping.match(eq(this.request), this.pattern.capture()))
				.thenReturn(this.result);
		this.matcher.setServletPath("/spring");
		this.request.setServletPath("/spring");

		assertThat(this.matcher.matches(this.request)).isTrue();
		assertThat(this.pattern.getValue()).isEqualTo("/path");
	}

	@Test
	public void matchesServletPathFalse() throws Exception {
		when(this.introspector.getMatchableHandlerMapping(this.request))
				.thenReturn(this.mapping);
		when(this.mapping.match(eq(this.request), this.pattern.capture()))
				.thenReturn(this.result);
		this.matcher.setServletPath("/spring");
		this.request.setServletPath("/");

		assertThat(this.matcher.matches(this.request)).isFalse();
	}

	@Test
	public void matchesPathOnlyTrue() throws Exception {
		when(this.introspector.getMatchableHandlerMapping(this.request))
				.thenReturn(this.mapping);
		when(this.mapping.match(eq(this.request), this.pattern.capture()))
				.thenReturn(this.result);

		assertThat(this.matcher.matches(this.request)).isTrue();
		assertThat(this.pattern.getValue()).isEqualTo("/path");
	}

	@Test
	public void matchesDefaultMatches() throws Exception {
		when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null);

		assertThat(this.matcher.matches(this.request)).isTrue();
	}

	@Test
	public void matchesDefaultDoesNotMatch() throws Exception {
		this.request.setServletPath("/other");
		when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null);

		assertThat(this.matcher.matches(this.request)).isFalse();
	}

	@Test
	public void matchesPathOnlyFalse() throws Exception {
		when(this.introspector.getMatchableHandlerMapping(this.request))
				.thenReturn(this.mapping);

		assertThat(this.matcher.matches(this.request)).isFalse();
	}

	@Test
	public void matchesMethodAndPathTrue() throws Exception {
		this.matcher.setMethod(HttpMethod.GET);
		when(this.introspector.getMatchableHandlerMapping(this.request))
				.thenReturn(this.mapping);
		when(this.mapping.match(eq(this.request), this.pattern.capture()))
				.thenReturn(this.result);

		assertThat(this.matcher.matches(this.request)).isTrue();
		assertThat(this.pattern.getValue()).isEqualTo("/path");
	}

	@Test
	public void matchesMethodAndPathFalseMethod() throws Exception {
		this.matcher.setMethod(HttpMethod.POST);

		assertThat(this.matcher.matches(this.request)).isFalse();
		// method compare should be done first since faster
		verifyZeroInteractions(this.introspector);
	}

	/**
	 * Malicious users can specify any HTTP Method to create a stacktrace and try to
	 * expose useful information about the system. We should ensure we ignore invalid HTTP
	 * methods.
	 * @throws Exception if an error occurs
	 */
	@Test
	public void matchesInvalidMethodOnRequest() throws Exception {
		this.matcher.setMethod(HttpMethod.GET);
		this.request.setMethod("invalid");

		assertThat(this.matcher.matches(this.request)).isFalse();
		// method compare should be done first since faster
		verifyZeroInteractions(this.introspector);
	}

	@Test
	public void matchesMethodAndPathFalsePath() throws Exception {
		this.matcher.setMethod(HttpMethod.GET);
		when(this.introspector.getMatchableHandlerMapping(this.request))
				.thenReturn(this.mapping);

		assertThat(this.matcher.matches(this.request)).isFalse();
	}

	@Test
	public void matchesGetMatchableHandlerMappingNull() throws Exception {
		assertThat(this.matcher.matches(this.request)).isTrue();
	}

	@Test
	public void matchesGetMatchableHandlerMappingThrows() throws Exception {
		when(this.introspector.getMatchableHandlerMapping(this.request)).thenThrow(
				new HttpRequestMethodNotSupportedException(this.request.getMethod()));
		assertThat(this.matcher.matches(this.request)).isTrue();
	}
}