/*
 * Copyright 2002-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.security.provisioning;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.PopulatedDatabase;
import org.springframework.security.TestDataSource;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetails;

/**
 * Tests for {@link JdbcUserDetailsManager}
 *
 * @author Luke Taylor
 */
public class JdbcUserDetailsManagerTests {
	private static final String SELECT_JOE_SQL = "select * from users where username = 'joe'";
	private static final String SELECT_JOE_AUTHORITIES_SQL = "select * from authorities where username = 'joe'";

	private static final UserDetails joe = new User("joe", "password", true, true, true,
			true, AuthorityUtils.createAuthorityList("A", "C", "B"));

	private static TestDataSource dataSource;
	private JdbcUserDetailsManager manager;
	private MockUserCache cache;
	private JdbcTemplate template;

	@BeforeClass
	public static void createDataSource() {
		dataSource = new TestDataSource("jdbcusermgrtest");
	}

	@AfterClass
	public static void clearDataSource() throws Exception {
		dataSource.destroy();
		dataSource = null;
	}

	@Before
	public void initializeManagerAndCreateTables() {
		manager = new JdbcUserDetailsManager();
		cache = new MockUserCache();
		manager.setUserCache(cache);
		manager.setDataSource(dataSource);
		manager.setCreateUserSql(JdbcUserDetailsManager.DEF_CREATE_USER_SQL);
		manager.setUpdateUserSql(JdbcUserDetailsManager.DEF_UPDATE_USER_SQL);
		manager.setUserExistsSql(JdbcUserDetailsManager.DEF_USER_EXISTS_SQL);
		manager.setCreateAuthoritySql(JdbcUserDetailsManager.DEF_INSERT_AUTHORITY_SQL);
		manager.setDeleteUserAuthoritiesSql(JdbcUserDetailsManager.DEF_DELETE_USER_AUTHORITIES_SQL);
		manager.setDeleteUserSql(JdbcUserDetailsManager.DEF_DELETE_USER_SQL);
		manager.setChangePasswordSql(JdbcUserDetailsManager.DEF_CHANGE_PASSWORD_SQL);
		manager.initDao();
		template = manager.getJdbcTemplate();

		template.execute("create table users(username varchar(20) not null primary key,"
				+ "password varchar(20) not null, enabled boolean not null)");
		template.execute("create table authorities (username varchar(20) not null, authority varchar(20) not null, "
				+ "constraint fk_authorities_users foreign key(username) references users(username))");
		PopulatedDatabase.createGroupTables(template);
		PopulatedDatabase.insertGroupData(template);
	}

	@After
	public void dropTablesAndClearContext() {
		template.execute("drop table authorities");
		template.execute("drop table users");
		template.execute("drop table group_authorities");
		template.execute("drop table group_members");
		template.execute("drop table groups");
		SecurityContextHolder.clearContext();
	}

	@Test
	public void createUserInsertsCorrectData() {
		manager.createUser(joe);

		UserDetails joe2 = manager.loadUserByUsername("joe");

		assertThat(joe2).isEqualTo(joe);
	}

	@Test
	public void deleteUserRemovesUserDataAndAuthoritiesAndClearsCache() {
		insertJoe();
		manager.deleteUser("joe");

		assertThat(template.queryForList(SELECT_JOE_SQL)).isEmpty();
		assertThat(template.queryForList(SELECT_JOE_AUTHORITIES_SQL)).isEmpty();
		assertThat(cache.getUserMap().containsKey("joe")).isFalse();
	}

	@Test
	public void updateUserChangesDataCorrectlyAndClearsCache() {
		insertJoe();
		User newJoe = new User("joe", "newpassword", false, true, true, true,
				AuthorityUtils.createAuthorityList(new String[] { "D", "F", "E" }));

		manager.updateUser(newJoe);

		UserDetails joe = manager.loadUserByUsername("joe");

		assertThat(joe).isEqualTo(newJoe);
		assertThat(cache.getUserMap().containsKey("joe")).isFalse();
	}

	@Test
	public void userExistsReturnsFalseForNonExistentUsername() {
		assertThat(manager.userExists("joe")).isFalse();
	}

	@Test
	public void userExistsReturnsTrueForExistingUsername() {
		insertJoe();
		assertThat(manager.userExists("joe")).isTrue();
		assertThat(cache.getUserMap().containsKey("joe")).isTrue();
	}

	@Test(expected = AccessDeniedException.class)
	public void changePasswordFailsForUnauthenticatedUser() {
		manager.changePassword("password", "newPassword");
	}

	@Test
	public void changePasswordSucceedsWithAuthenticatedUserAndNoAuthenticationManagerSet() {
		insertJoe();
		authenticateJoe();
		manager.changePassword("wrongpassword", "newPassword");
		UserDetails newJoe = manager.loadUserByUsername("joe");

		assertThat(newJoe.getPassword()).isEqualTo("newPassword");
		assertThat(cache.getUserMap().containsKey("joe")).isFalse();
	}

	@Test
	public void changePasswordSucceedsWithIfReAuthenticationSucceeds() {
		insertJoe();
		Authentication currentAuth = authenticateJoe();
		AuthenticationManager am = mock(AuthenticationManager.class);
		when(am.authenticate(currentAuth)).thenReturn(currentAuth);

		manager.setAuthenticationManager(am);
		manager.changePassword("password", "newPassword");
		UserDetails newJoe = manager.loadUserByUsername("joe");

		assertThat(newJoe.getPassword()).isEqualTo("newPassword");
		// The password in the context should also be altered
		Authentication newAuth = SecurityContextHolder.getContext().getAuthentication();
		assertThat(newAuth.getName()).isEqualTo("joe");
		assertThat(newAuth.getDetails()).isEqualTo(currentAuth.getDetails());
		assertThat(newAuth.getCredentials()).isNull();
		assertThat(cache.getUserMap().containsKey("joe")).isFalse();
	}

	@Test
	public void changePasswordFailsIfReAuthenticationFails() {
		insertJoe();
		authenticateJoe();
		AuthenticationManager am = mock(AuthenticationManager.class);
		when(am.authenticate(any(Authentication.class))).thenThrow(
				new BadCredentialsException(""));

		manager.setAuthenticationManager(am);

		try {
			manager.changePassword("password", "newPassword");
			fail("Expected BadCredentialsException");
		}
		catch (BadCredentialsException expected) {
		}

		// Check password hasn't changed.
		UserDetails newJoe = manager.loadUserByUsername("joe");
		assertThat(newJoe.getPassword()).isEqualTo("password");
		assertThat(SecurityContextHolder.getContext().getAuthentication().getCredentials()).isEqualTo("password");
		assertThat(cache.getUserMap().containsKey("joe")).isTrue();
	}

	@Test
	public void findAllGroupsReturnsExpectedGroupNames() {
		List<String> groups = manager.findAllGroups();
		assertThat(groups).hasSize(4);

		Collections.sort(groups);
		assertThat(groups.get(0)).isEqualTo("GROUP_0");
		assertThat(groups.get(1)).isEqualTo("GROUP_1");
		assertThat(groups.get(2)).isEqualTo("GROUP_2");
		assertThat(groups.get(3)).isEqualTo("GROUP_3");
	}

	@Test
	public void findGroupMembersReturnsCorrectData() {
		List<String> groupMembers = manager.findUsersInGroup("GROUP_0");
		assertThat(groupMembers).hasSize(1);
		assertThat(groupMembers.get(0)).isEqualTo("jerry");
		groupMembers = manager.findUsersInGroup("GROUP_1");
		assertThat(groupMembers).hasSize(2);
	}

	@Test
	@SuppressWarnings("unchecked")
	public void createGroupInsertsCorrectData() {
		manager.createGroup("TEST_GROUP",
				AuthorityUtils.createAuthorityList("ROLE_X", "ROLE_Y"));

		List roles = template
				.queryForList("select ga.authority from groups g, group_authorities ga "
						+ "where ga.group_id = g.id " + "and g.group_name = 'TEST_GROUP'");

		assertThat(roles).hasSize(2);
	}

	@Test
	public void deleteGroupRemovesData() throws Exception {
		manager.deleteGroup("GROUP_0");
		manager.deleteGroup("GROUP_1");
		manager.deleteGroup("GROUP_2");
		manager.deleteGroup("GROUP_3");

		assertThat(template.queryForList("select * from group_authorities")).isEmpty();
		assertThat(template.queryForList("select * from group_members")).isEmpty();
		assertThat(template.queryForList("select id from groups")).isEmpty();
	}

	@Test
	public void renameGroupIsSuccessful() throws Exception {
		manager.renameGroup("GROUP_0", "GROUP_X");

		assertThat(template.queryForObject("select id from groups where group_name = 'GROUP_X'",
				Integer.class)).isZero();
	}

	@Test
	public void addingGroupUserSetsCorrectData() throws Exception {
		manager.addUserToGroup("tom", "GROUP_0");

		assertThat(
				template.queryForList(
						"select username from group_members where group_id = 0")).hasSize(2);
	}

	@Test
	public void removeUserFromGroupDeletesGroupMemberRow() throws Exception {
		manager.removeUserFromGroup("jerry", "GROUP_1");

		assertThat(
								template.queryForList(
						"select group_id from group_members where username = 'jerry'")).hasSize(1);
	}

	@Test
	public void findGroupAuthoritiesReturnsCorrectAuthorities() throws Exception {
		assertThat(AuthorityUtils.createAuthorityList("ROLE_A")).isEqualTo(manager.findGroupAuthorities("GROUP_0"));
	}

	@Test
	public void addGroupAuthorityInsertsCorrectGroupAuthorityRow() throws Exception {
		GrantedAuthority auth = new SimpleGrantedAuthority("ROLE_X");
		manager.addGroupAuthority("GROUP_0", auth);

		template.queryForObject(
				"select authority from group_authorities where authority = 'ROLE_X' and group_id = 0",
				String.class);
	}

	@Test
	public void deleteGroupAuthorityRemovesCorrectRows() throws Exception {
		GrantedAuthority auth = new SimpleGrantedAuthority("ROLE_A");
		manager.removeGroupAuthority("GROUP_0", auth);
		assertThat(
				template.queryForList(
						"select authority from group_authorities where group_id = 0")).isEmpty();

		manager.removeGroupAuthority("GROUP_2", auth);
		assertThat(
				template.queryForList(
						"select authority from group_authorities where group_id = 2")).hasSize(2);
	}

	// SEC-1156
	@Test
	public void createUserDoesNotSaveAuthoritiesIfEnableAuthoritiesIsFalse()
			throws Exception {
		manager.setEnableAuthorities(false);
		manager.createUser(joe);
		assertThat(template.queryForList(SELECT_JOE_AUTHORITIES_SQL)).isEmpty();
	}

	// SEC-1156
	@Test
	public void updateUserDoesNotSaveAuthoritiesIfEnableAuthoritiesIsFalse()
			throws Exception {
		manager.setEnableAuthorities(false);
		insertJoe();
		template.execute("delete from authorities where username='joe'");
		manager.updateUser(joe);
		assertThat(template.queryForList(SELECT_JOE_AUTHORITIES_SQL)).isEmpty();
	}

	// SEC-2166
	@Test
	public void createNewAuthenticationUsesNullPasswordToKeepPassordsSave() {
		insertJoe();
		UsernamePasswordAuthenticationToken currentAuth = new UsernamePasswordAuthenticationToken(
				"joe", null, AuthorityUtils.createAuthorityList("ROLE_USER"));
		Authentication updatedAuth = manager.createNewAuthentication(currentAuth, "new");
		assertThat(updatedAuth.getCredentials()).isNull();
	}

	private Authentication authenticateJoe() {
		UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
				"joe", "password", joe.getAuthorities());
		SecurityContextHolder.getContext().setAuthentication(auth);

		return auth;
	}

	private void insertJoe() {
		template.execute("insert into users (username, password, enabled) values ('joe','password','true')");
		template.execute("insert into authorities (username, authority) values ('joe','A')");
		template.execute("insert into authorities (username, authority) values ('joe','B')");
		template.execute("insert into authorities (username, authority) values ('joe','C')");
		cache.putUserInCache(joe);
	}

	private class MockUserCache implements UserCache {
		private Map<String, UserDetails> cache = new HashMap<>();

		public UserDetails getUserFromCache(String username) {
			return (User) cache.get(username);
		}

		public void putUserInCache(UserDetails user) {
			cache.put(user.getUsername(), user);
		}

		public void removeUserFromCache(String username) {
			cache.remove(username);
		}

		Map<String, UserDetails> getUserMap() {
			return cache;
		}
	}
}
