/*
 * 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.cloudfoundry;

import java.util.Map;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import org.springframework.boot.actuate.cloudfoundry.CloudFoundryAuthorizationException.Reason;
import org.springframework.boot.test.web.client.MockServerRestTemplateCustomizer;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withUnauthorizedRequest;

/**
 * Tests for {@link CloudFoundrySecurityService}.
 *
 * @author Madhura Bhave
 */
public class CloudFoundrySecurityServiceTests {

	@Rule
	public ExpectedException thrown = ExpectedException.none();

	private static final String CLOUD_CONTROLLER = "http://my-cloud-controller.com";

	private static final String CLOUD_CONTROLLER_PERMISSIONS = CLOUD_CONTROLLER
			+ "/v2/apps/my-app-id/permissions";

	private static final String UAA_URL = "http://my-uaa.com";

	private CloudFoundrySecurityService securityService;

	private MockRestServiceServer server;

	@Before
	public void setup() throws Exception {
		MockServerRestTemplateCustomizer mockServerCustomizer = new MockServerRestTemplateCustomizer();
		RestTemplateBuilder builder = new RestTemplateBuilder(mockServerCustomizer);
		this.securityService = new CloudFoundrySecurityService(builder, CLOUD_CONTROLLER,
				false);
		this.server = mockServerCustomizer.getServer();
	}

	@Test
	public void skipSslValidationWhenTrue() throws Exception {
		RestTemplateBuilder builder = new RestTemplateBuilder();
		this.securityService = new CloudFoundrySecurityService(builder, CLOUD_CONTROLLER,
				true);
		RestTemplate restTemplate = (RestTemplate) ReflectionTestUtils
				.getField(this.securityService, "restTemplate");
		assertThat(restTemplate.getRequestFactory())
				.isInstanceOf(SkipSslVerificationHttpRequestFactory.class);
	}

	@Test
	public void doNotskipSslValidationWhenFalse() throws Exception {
		RestTemplateBuilder builder = new RestTemplateBuilder();
		this.securityService = new CloudFoundrySecurityService(builder, CLOUD_CONTROLLER,
				false);
		RestTemplate restTemplate = (RestTemplate) ReflectionTestUtils
				.getField(this.securityService, "restTemplate");
		assertThat(restTemplate.getRequestFactory())
				.isNotInstanceOf(SkipSslVerificationHttpRequestFactory.class);
	}

	@Test
	public void getAccessLevelWhenSpaceDeveloperShouldReturnFull() throws Exception {
		String responseBody = "{\"read_sensitive_data\": true,\"read_basic_data\": true}";
		this.server.expect(requestTo(CLOUD_CONTROLLER_PERMISSIONS))
				.andExpect(header("Authorization", "bearer my-access-token"))
				.andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON));
		AccessLevel accessLevel = this.securityService.getAccessLevel("my-access-token",
				"my-app-id");
		this.server.verify();
		assertThat(accessLevel).isEqualTo(AccessLevel.FULL);
	}

	@Test
	public void getAccessLevelWhenNotSpaceDeveloperShouldReturnRestricted()
			throws Exception {
		String responseBody = "{\"read_sensitive_data\": false,\"read_basic_data\": true}";
		this.server.expect(requestTo(CLOUD_CONTROLLER_PERMISSIONS))
				.andExpect(header("Authorization", "bearer my-access-token"))
				.andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON));
		AccessLevel accessLevel = this.securityService.getAccessLevel("my-access-token",
				"my-app-id");
		this.server.verify();
		assertThat(accessLevel).isEqualTo(AccessLevel.RESTRICTED);
	}

	@Test
	public void getAccessLevelWhenTokenIsNotValidShouldThrowException() throws Exception {
		this.server.expect(requestTo(CLOUD_CONTROLLER_PERMISSIONS))
				.andExpect(header("Authorization", "bearer my-access-token"))
				.andRespond(withUnauthorizedRequest());
		this.thrown
				.expect(AuthorizationExceptionMatcher.withReason(Reason.INVALID_TOKEN));
		this.securityService.getAccessLevel("my-access-token", "my-app-id");
	}

	@Test
	public void getAccessLevelWhenForbiddenShouldThrowException() throws Exception {
		this.server.expect(requestTo(CLOUD_CONTROLLER_PERMISSIONS))
				.andExpect(header("Authorization", "bearer my-access-token"))
				.andRespond(withStatus(HttpStatus.FORBIDDEN));
		this.thrown
				.expect(AuthorizationExceptionMatcher.withReason(Reason.ACCESS_DENIED));
		this.securityService.getAccessLevel("my-access-token", "my-app-id");
	}

	@Test
	public void getAccessLevelWhenCloudControllerIsNotReachableThrowsException()
			throws Exception {
		this.server.expect(requestTo(CLOUD_CONTROLLER_PERMISSIONS))
				.andExpect(header("Authorization", "bearer my-access-token"))
				.andRespond(withServerError());
		this.thrown.expect(
				AuthorizationExceptionMatcher.withReason(Reason.SERVICE_UNAVAILABLE));
		this.securityService.getAccessLevel("my-access-token", "my-app-id");
	}

	@Test
	public void fetchTokenKeysWhenSuccessfulShouldReturnListOfKeysFromUAA()
			throws Exception {
		this.server.expect(requestTo(CLOUD_CONTROLLER + "/info"))
				.andRespond(withSuccess("{\"token_endpoint\":\"http://my-uaa.com\"}",
						MediaType.APPLICATION_JSON));
		String tokenKeyValue = "-----BEGIN PUBLIC KEY-----\n"
				+ "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0m59l2u9iDnMbrXHfqkO\n"
				+ "rn2dVQ3vfBJqcDuFUK03d+1PZGbVlNCqnkpIJ8syFppW8ljnWweP7+LiWpRoz0I7\n"
				+ "fYb3d8TjhV86Y997Fl4DBrxgM6KTJOuE/uxnoDhZQ14LgOU2ckXjOzOdTsnGMKQB\n"
				+ "LCl0vpcXBtFLMaSbpv1ozi8h7DJyVZ6EnFQZUWGdgTMhDrmqevfx95U/16c5WBDO\n"
				+ "kqwIn7Glry9n9Suxygbf8g5AzpWcusZgDLIIZ7JTUldBb8qU2a0Dl4mvLZOn4wPo\n"
				+ "jfj9Cw2QICsc5+Pwf21fP+hzf+1WSRHbnYv8uanRO0gZ8ekGaghM/2H6gqJbo2nI\n"
				+ "JwIDAQAB\n-----END PUBLIC KEY-----";
		String responseBody = "{\"keys\" : [ {\"kid\":\"test-key\",\"value\" : \""
				+ tokenKeyValue.replace("\n", "\\n") + "\"} ]}";
		this.server.expect(requestTo(UAA_URL + "/token_keys"))
				.andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON));
		Map<String, String> tokenKeys = this.securityService.fetchTokenKeys();
		this.server.verify();
		assertThat(tokenKeys.get("test-key")).isEqualTo(tokenKeyValue);
	}

	@Test
	public void fetchTokenKeysWhenNoKeysReturnedFromUAA() throws Exception {
		this.server.expect(requestTo(CLOUD_CONTROLLER + "/info")).andRespond(withSuccess(
				"{\"token_endpoint\":\"" + UAA_URL + "\"}", MediaType.APPLICATION_JSON));
		String responseBody = "{\"keys\": []}";
		this.server.expect(requestTo(UAA_URL + "/token_keys"))
				.andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON));
		Map<String, String> tokenKeys = this.securityService.fetchTokenKeys();
		this.server.verify();
		assertThat(tokenKeys).hasSize(0);
	}

	@Test
	public void fetchTokenKeysWhenUnsuccessfulShouldThrowException() throws Exception {
		this.server.expect(requestTo(CLOUD_CONTROLLER + "/info")).andRespond(withSuccess(
				"{\"token_endpoint\":\"" + UAA_URL + "\"}", MediaType.APPLICATION_JSON));
		this.server.expect(requestTo(UAA_URL + "/token_keys"))
				.andRespond(withServerError());
		this.thrown.expect(
				AuthorizationExceptionMatcher.withReason(Reason.SERVICE_UNAVAILABLE));
		this.securityService.fetchTokenKeys();
	}

	@Test
	public void getUaaUrlShouldCallCloudControllerInfoOnlyOnce() throws Exception {
		this.server.expect(requestTo(CLOUD_CONTROLLER + "/info")).andRespond(withSuccess(
				"{\"token_endpoint\":\"" + UAA_URL + "\"}", MediaType.APPLICATION_JSON));
		String uaaUrl = this.securityService.getUaaUrl();
		this.server.verify();
		assertThat(uaaUrl).isEqualTo(UAA_URL);
		// Second call should not need to hit server
		uaaUrl = this.securityService.getUaaUrl();
		assertThat(uaaUrl).isEqualTo(UAA_URL);
	}

	@Test
	public void getUaaUrlWhenCloudControllerUrlIsNotReachableShouldThrowException()
			throws Exception {
		this.server.expect(requestTo(CLOUD_CONTROLLER + "/info"))
				.andRespond(withServerError());
		this.thrown.expect(
				AuthorizationExceptionMatcher.withReason(Reason.SERVICE_UNAVAILABLE));
		this.securityService.getUaaUrl();
	}

}
